diff --git a/Cargo.lock b/Cargo.lock index 11bbe4f6930..cb226f711a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8907,6 +8907,8 @@ version = "0.1.0" dependencies = [ "anyhow", "async-std", + "chrono", + "cuid", "futures-util", "lazy_static", "local-ip-addr", diff --git a/cli/src/api/rockbox.v1alpha1.rs b/cli/src/api/rockbox.v1alpha1.rs index 2dc76196977..b4198d07926 100644 --- a/cli/src/api/rockbox.v1alpha1.rs +++ b/cli/src/api/rockbox.v1alpha1.rs @@ -4316,23 +4316,30 @@ pub struct StartResponse {} pub struct SyncRequest {} #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct SyncResponse {} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct RemoveAllTracksRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveAllTracksRequest { + #[prost(string, optional, tag = "1")] + pub playlist_id: ::core::option::Option<::prost::alloc::string::String>, +} #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct RemoveAllTracksResponse {} #[derive(Clone, PartialEq, ::prost::Message)] pub struct RemoveTracksRequest { #[prost(int32, repeated, tag = "1")] pub positions: ::prost::alloc::vec::Vec, + #[prost(string, optional, tag = "2")] + pub playlist_id: ::core::option::Option<::prost::alloc::string::String>, } #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct RemoveTracksResponse {} #[derive(Clone, PartialEq, ::prost::Message)] pub struct CreatePlaylistRequest { - #[prost(string, tag = "1")] - pub name: ::prost::alloc::string::String, + #[prost(string, optional, tag = "1")] + pub name: ::core::option::Option<::prost::alloc::string::String>, #[prost(string, repeated, tag = "2")] pub tracks: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(string, optional, tag = "3")] + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, } #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct CreatePlaylistResponse { @@ -4388,6 +4395,8 @@ pub struct InsertAlbumRequest { pub album_id: ::prost::alloc::string::String, #[prost(bool, optional, tag = "3")] pub shuffle: ::core::option::Option, + #[prost(string, optional, tag = "4")] + pub playlist_id: ::core::option::Option<::prost::alloc::string::String>, } #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct InsertAlbumResponse {} @@ -4399,6 +4408,8 @@ pub struct InsertArtistTracksRequest { pub artist_id: ::prost::alloc::string::String, #[prost(bool, optional, tag = "3")] pub shuffle: ::core::option::Option, + #[prost(string, optional, tag = "4")] + pub playlist_id: ::core::option::Option<::prost::alloc::string::String>, } #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct InsertArtistTracksResponse {} @@ -4409,6 +4420,128 @@ pub struct ShufflePlaylistRequest { } #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct ShufflePlaylistResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPlaylistRequest { + #[prost(string, tag = "1")] + pub playlist_id: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPlaylistResponse { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, + #[prost(string, optional, tag = "3")] + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "4")] + pub image: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "5")] + pub description: ::core::option::Option<::prost::alloc::string::String>, + #[prost(int32, tag = "6")] + pub amount: i32, + #[prost(string, tag = "7")] + pub created_at: ::prost::alloc::string::String, + #[prost(string, tag = "8")] + pub updated_at: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "9")] + pub tracks: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CreateFolderRequest { + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + #[prost(string, optional, tag = "2")] + pub parent_id: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CreateFolderResponse { + #[prost(string, tag = "1")] + pub folder_id: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetFolderRequest { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetFolderResponse { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, + #[prost(string, optional, tag = "3")] + pub parent_id: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetFoldersRequest { + #[prost(string, optional, tag = "1")] + pub parent_id: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetFoldersResponse { + #[prost(message, repeated, tag = "1")] + pub folders: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveFolderRequest { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct RemoveFolderResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemovePlaylistRequest { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct RemovePlaylistResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RenamePlaylistRequest { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct RenamePlaylistResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RenameFolderRequest { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct RenameFolderResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MovePlaylistRequest { + #[prost(string, tag = "1")] + pub playlist_id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub folder_id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct MovePlaylistResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MoveFolderRequest { + #[prost(string, tag = "1")] + pub folder_id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub parent_id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct MoveFolderResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPlaylistsRequest { + #[prost(string, optional, tag = "1")] + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPlaylistsResponse { + #[prost(message, repeated, tag = "1")] + pub playlists: ::prost::alloc::vec::Vec, +} /// Generated client implementations. pub mod playlist_service_client { #![allow( @@ -5020,78 +5153,364 @@ pub mod playlist_service_client { ); self.inner.unary(req, path, codec).await } - } -} -/// Generated server implementations. -pub mod playlist_service_server { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tonic::codegen::*; - /// Generated trait containing gRPC methods that should be implemented for use with PlaylistServiceServer. - #[async_trait] - pub trait PlaylistService: std::marker::Send + std::marker::Sync + 'static { - async fn get_current( - &self, - request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; - async fn get_resume_info( - &self, - request: tonic::Request, + pub async fn get_playlist( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn get_track_info( - &self, - request: tonic::Request, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/GetPlaylist", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "GetPlaylist"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn get_playlists( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn get_first_index( - &self, - request: tonic::Request, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/GetPlaylists", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "GetPlaylists"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn create_folder( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn get_display_index( - &self, - request: tonic::Request, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/CreateFolder", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "CreateFolder"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn get_folder( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn amount( - &self, - request: tonic::Request, - ) -> std::result::Result, tonic::Status>; - async fn playlist_resume( - &self, - request: tonic::Request, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/GetFolder", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "GetFolder"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn get_folders( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn resume_track( - &self, - request: tonic::Request, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/GetFolders", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "GetFolders"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn remove_folder( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn set_modified( - &self, - request: tonic::Request, - ) -> std::result::Result< + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/RemoveFolder", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "RemoveFolder"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn remove_playlist( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/RemovePlaylist", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "RemovePlaylist"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn rename_playlist( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/RenamePlaylist", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "RenamePlaylist"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn rename_folder( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/RenameFolder", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "RenameFolder"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn move_playlist( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/MovePlaylist", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "MovePlaylist"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn move_folder( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/MoveFolder", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "MoveFolder"), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod playlist_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with PlaylistServiceServer. + #[async_trait] + pub trait PlaylistService: std::marker::Send + std::marker::Sync + 'static { + async fn get_current( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_resume_info( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_track_info( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_first_index( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_display_index( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn amount( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn playlist_resume( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn resume_track( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn set_modified( + &self, + request: tonic::Request, + ) -> std::result::Result< tonic::Response, tonic::Status, >; @@ -5166,6 +5585,83 @@ pub mod playlist_service_server { tonic::Response, tonic::Status, >; + async fn get_playlist( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_playlists( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn create_folder( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_folder( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_folders( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn remove_folder( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn remove_playlist( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn rename_playlist( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn rename_folder( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn move_playlist( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn move_folder( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } #[derive(Debug)] pub struct PlaylistServiceServer { @@ -5245,23 +5741,522 @@ pub mod playlist_service_server { match req.uri().path() { "/rockbox.v1alpha1.PlaylistService/GetCurrent" => { #[allow(non_camel_case_types)] - struct GetCurrentSvc(pub Arc); + struct GetCurrentSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for GetCurrentSvc { + type Response = super::GetCurrentResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_current(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetCurrentSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/GetResumeInfo" => { + #[allow(non_camel_case_types)] + struct GetResumeInfoSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for GetResumeInfoSvc { + type Response = super::GetResumeInfoResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_resume_info(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetResumeInfoSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/GetTrackInfo" => { + #[allow(non_camel_case_types)] + struct GetTrackInfoSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for GetTrackInfoSvc { + type Response = super::GetTrackInfoResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_track_info(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetTrackInfoSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/GetFirstIndex" => { + #[allow(non_camel_case_types)] + struct GetFirstIndexSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for GetFirstIndexSvc { + type Response = super::GetFirstIndexResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_first_index(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetFirstIndexSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/GetDisplayIndex" => { + #[allow(non_camel_case_types)] + struct GetDisplayIndexSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for GetDisplayIndexSvc { + type Response = super::GetDisplayIndexResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_display_index(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetDisplayIndexSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/Amount" => { + #[allow(non_camel_case_types)] + struct AmountSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for AmountSvc { + type Response = super::AmountResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::amount(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = AmountSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/PlaylistResume" => { + #[allow(non_camel_case_types)] + struct PlaylistResumeSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for PlaylistResumeSvc { + type Response = super::PlaylistResumeResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::playlist_resume(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = PlaylistResumeSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/ResumeTrack" => { + #[allow(non_camel_case_types)] + struct ResumeTrackSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for ResumeTrackSvc { + type Response = super::ResumeTrackResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::resume_track(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ResumeTrackSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/SetModified" => { + #[allow(non_camel_case_types)] + struct SetModifiedSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for SetModifiedSvc { + type Response = super::SetModifiedResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::set_modified(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SetModifiedSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/Start" => { + #[allow(non_camel_case_types)] + struct StartSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService for StartSvc { + type Response = super::StartResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::start(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = StartSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/Sync" => { + #[allow(non_camel_case_types)] + struct SyncSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService for SyncSvc { + type Response = super::SyncResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::sync(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SyncSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/RemoveAllTracks" => { + #[allow(non_camel_case_types)] + struct RemoveAllTracksSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for GetCurrentSvc { - type Response = super::GetCurrentResponse; + > tonic::server::UnaryService + for RemoveAllTracksSvc { + type Response = super::RemoveAllTracksResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_current(&inner, request).await + ::remove_all_tracks(&inner, request) + .await }; Box::pin(fut) } @@ -5272,7 +6267,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetCurrentSvc(inner); + let method = RemoveAllTracksSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5288,26 +6283,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/GetResumeInfo" => { + "/rockbox.v1alpha1.PlaylistService/RemoveTracks" => { #[allow(non_camel_case_types)] - struct GetResumeInfoSvc(pub Arc); + struct RemoveTracksSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for GetResumeInfoSvc { - type Response = super::GetResumeInfoResponse; + > tonic::server::UnaryService + for RemoveTracksSvc { + type Response = super::RemoveTracksResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_resume_info(&inner, request) - .await + ::remove_tracks(&inner, request).await }; Box::pin(fut) } @@ -5318,7 +6312,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetResumeInfoSvc(inner); + let method = RemoveTracksSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5334,25 +6328,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/GetTrackInfo" => { + "/rockbox.v1alpha1.PlaylistService/CreatePlaylist" => { #[allow(non_camel_case_types)] - struct GetTrackInfoSvc(pub Arc); + struct CreatePlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for GetTrackInfoSvc { - type Response = super::GetTrackInfoResponse; + > tonic::server::UnaryService + for CreatePlaylistSvc { + type Response = super::CreatePlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_track_info(&inner, request) + ::create_playlist(&inner, request) .await }; Box::pin(fut) @@ -5364,7 +6358,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetTrackInfoSvc(inner); + let method = CreatePlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5380,26 +6374,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/GetFirstIndex" => { + "/rockbox.v1alpha1.PlaylistService/InsertTracks" => { #[allow(non_camel_case_types)] - struct GetFirstIndexSvc(pub Arc); + struct InsertTracksSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for GetFirstIndexSvc { - type Response = super::GetFirstIndexResponse; + > tonic::server::UnaryService + for InsertTracksSvc { + type Response = super::InsertTracksResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_first_index(&inner, request) - .await + ::insert_tracks(&inner, request).await }; Box::pin(fut) } @@ -5410,7 +6403,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetFirstIndexSvc(inner); + let method = InsertTracksSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5426,25 +6419,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/GetDisplayIndex" => { + "/rockbox.v1alpha1.PlaylistService/InsertDirectory" => { #[allow(non_camel_case_types)] - struct GetDisplayIndexSvc(pub Arc); + struct InsertDirectorySvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for GetDisplayIndexSvc { - type Response = super::GetDisplayIndexResponse; + > tonic::server::UnaryService + for InsertDirectorySvc { + type Response = super::InsertDirectoryResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_display_index(&inner, request) + ::insert_directory(&inner, request) .await }; Box::pin(fut) @@ -5456,7 +6449,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetDisplayIndexSvc(inner); + let method = InsertDirectorySvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5472,25 +6465,26 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/Amount" => { + "/rockbox.v1alpha1.PlaylistService/InsertPlaylist" => { #[allow(non_camel_case_types)] - struct AmountSvc(pub Arc); + struct InsertPlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for AmountSvc { - type Response = super::AmountResponse; + > tonic::server::UnaryService + for InsertPlaylistSvc { + type Response = super::InsertPlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::amount(&inner, request).await + ::insert_playlist(&inner, request) + .await }; Box::pin(fut) } @@ -5501,7 +6495,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = AmountSvc(inner); + let method = InsertPlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5517,26 +6511,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/PlaylistResume" => { + "/rockbox.v1alpha1.PlaylistService/InsertAlbum" => { #[allow(non_camel_case_types)] - struct PlaylistResumeSvc(pub Arc); + struct InsertAlbumSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for PlaylistResumeSvc { - type Response = super::PlaylistResumeResponse; + > tonic::server::UnaryService + for InsertAlbumSvc { + type Response = super::InsertAlbumResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::playlist_resume(&inner, request) - .await + ::insert_album(&inner, request).await }; Box::pin(fut) } @@ -5547,7 +6540,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = PlaylistResumeSvc(inner); + let method = InsertAlbumSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5563,25 +6556,29 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/ResumeTrack" => { + "/rockbox.v1alpha1.PlaylistService/InsertArtistTracks" => { #[allow(non_camel_case_types)] - struct ResumeTrackSvc(pub Arc); + struct InsertArtistTracksSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for ResumeTrackSvc { - type Response = super::ResumeTrackResponse; + > tonic::server::UnaryService + for InsertArtistTracksSvc { + type Response = super::InsertArtistTracksResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::resume_track(&inner, request).await + ::insert_artist_tracks( + &inner, + request, + ) + .await }; Box::pin(fut) } @@ -5592,7 +6589,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = ResumeTrackSvc(inner); + let method = InsertArtistTracksSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5608,25 +6605,26 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/SetModified" => { + "/rockbox.v1alpha1.PlaylistService/ShufflePlaylist" => { #[allow(non_camel_case_types)] - struct SetModifiedSvc(pub Arc); + struct ShufflePlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for SetModifiedSvc { - type Response = super::SetModifiedResponse; + > tonic::server::UnaryService + for ShufflePlaylistSvc { + type Response = super::ShufflePlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::set_modified(&inner, request).await + ::shuffle_playlist(&inner, request) + .await }; Box::pin(fut) } @@ -5637,7 +6635,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = SetModifiedSvc(inner); + let method = ShufflePlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5653,24 +6651,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/Start" => { + "/rockbox.v1alpha1.PlaylistService/GetPlaylist" => { #[allow(non_camel_case_types)] - struct StartSvc(pub Arc); + struct GetPlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService for StartSvc { - type Response = super::StartResponse; + > tonic::server::UnaryService + for GetPlaylistSvc { + type Response = super::GetPlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::start(&inner, request).await + ::get_playlist(&inner, request).await }; Box::pin(fut) } @@ -5681,7 +6680,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = StartSvc(inner); + let method = GetPlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5697,24 +6696,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/Sync" => { + "/rockbox.v1alpha1.PlaylistService/GetPlaylists" => { #[allow(non_camel_case_types)] - struct SyncSvc(pub Arc); + struct GetPlaylistsSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService for SyncSvc { - type Response = super::SyncResponse; + > tonic::server::UnaryService + for GetPlaylistsSvc { + type Response = super::GetPlaylistsResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::sync(&inner, request).await + ::get_playlists(&inner, request).await }; Box::pin(fut) } @@ -5725,7 +6725,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = SyncSvc(inner); + let method = GetPlaylistsSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5741,26 +6741,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/RemoveAllTracks" => { + "/rockbox.v1alpha1.PlaylistService/CreateFolder" => { #[allow(non_camel_case_types)] - struct RemoveAllTracksSvc(pub Arc); + struct CreateFolderSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for RemoveAllTracksSvc { - type Response = super::RemoveAllTracksResponse; + > tonic::server::UnaryService + for CreateFolderSvc { + type Response = super::CreateFolderResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::remove_all_tracks(&inner, request) - .await + ::create_folder(&inner, request).await }; Box::pin(fut) } @@ -5771,7 +6770,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = RemoveAllTracksSvc(inner); + let method = CreateFolderSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5787,25 +6786,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/RemoveTracks" => { + "/rockbox.v1alpha1.PlaylistService/GetFolder" => { #[allow(non_camel_case_types)] - struct RemoveTracksSvc(pub Arc); + struct GetFolderSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for RemoveTracksSvc { - type Response = super::RemoveTracksResponse; + > tonic::server::UnaryService + for GetFolderSvc { + type Response = super::GetFolderResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::remove_tracks(&inner, request).await + ::get_folder(&inner, request).await }; Box::pin(fut) } @@ -5816,7 +6815,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = RemoveTracksSvc(inner); + let method = GetFolderSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5832,26 +6831,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/CreatePlaylist" => { + "/rockbox.v1alpha1.PlaylistService/GetFolders" => { #[allow(non_camel_case_types)] - struct CreatePlaylistSvc(pub Arc); + struct GetFoldersSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for CreatePlaylistSvc { - type Response = super::CreatePlaylistResponse; + > tonic::server::UnaryService + for GetFoldersSvc { + type Response = super::GetFoldersResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::create_playlist(&inner, request) - .await + ::get_folders(&inner, request).await }; Box::pin(fut) } @@ -5862,7 +6860,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = CreatePlaylistSvc(inner); + let method = GetFoldersSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5878,25 +6876,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/InsertTracks" => { + "/rockbox.v1alpha1.PlaylistService/RemoveFolder" => { #[allow(non_camel_case_types)] - struct InsertTracksSvc(pub Arc); + struct RemoveFolderSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for InsertTracksSvc { - type Response = super::InsertTracksResponse; + > tonic::server::UnaryService + for RemoveFolderSvc { + type Response = super::RemoveFolderResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::insert_tracks(&inner, request).await + ::remove_folder(&inner, request).await }; Box::pin(fut) } @@ -5907,7 +6905,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = InsertTracksSvc(inner); + let method = RemoveFolderSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5923,25 +6921,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/InsertDirectory" => { + "/rockbox.v1alpha1.PlaylistService/RemovePlaylist" => { #[allow(non_camel_case_types)] - struct InsertDirectorySvc(pub Arc); + struct RemovePlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for InsertDirectorySvc { - type Response = super::InsertDirectoryResponse; + > tonic::server::UnaryService + for RemovePlaylistSvc { + type Response = super::RemovePlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::insert_directory(&inner, request) + ::remove_playlist(&inner, request) .await }; Box::pin(fut) @@ -5953,7 +6951,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = InsertDirectorySvc(inner); + let method = RemovePlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5969,25 +6967,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/InsertPlaylist" => { + "/rockbox.v1alpha1.PlaylistService/RenamePlaylist" => { #[allow(non_camel_case_types)] - struct InsertPlaylistSvc(pub Arc); + struct RenamePlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for InsertPlaylistSvc { - type Response = super::InsertPlaylistResponse; + > tonic::server::UnaryService + for RenamePlaylistSvc { + type Response = super::RenamePlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::insert_playlist(&inner, request) + ::rename_playlist(&inner, request) .await }; Box::pin(fut) @@ -5999,7 +6997,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = InsertPlaylistSvc(inner); + let method = RenamePlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6015,25 +7013,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/InsertAlbum" => { + "/rockbox.v1alpha1.PlaylistService/RenameFolder" => { #[allow(non_camel_case_types)] - struct InsertAlbumSvc(pub Arc); + struct RenameFolderSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for InsertAlbumSvc { - type Response = super::InsertAlbumResponse; + > tonic::server::UnaryService + for RenameFolderSvc { + type Response = super::RenameFolderResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::insert_album(&inner, request).await + ::rename_folder(&inner, request).await }; Box::pin(fut) } @@ -6044,7 +7042,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = InsertAlbumSvc(inner); + let method = RenameFolderSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6060,29 +7058,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/InsertArtistTracks" => { + "/rockbox.v1alpha1.PlaylistService/MovePlaylist" => { #[allow(non_camel_case_types)] - struct InsertArtistTracksSvc(pub Arc); + struct MovePlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for InsertArtistTracksSvc { - type Response = super::InsertArtistTracksResponse; + > tonic::server::UnaryService + for MovePlaylistSvc { + type Response = super::MovePlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::insert_artist_tracks( - &inner, - request, - ) - .await + ::move_playlist(&inner, request).await }; Box::pin(fut) } @@ -6093,7 +7087,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = InsertArtistTracksSvc(inner); + let method = MovePlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6109,26 +7103,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/ShufflePlaylist" => { + "/rockbox.v1alpha1.PlaylistService/MoveFolder" => { #[allow(non_camel_case_types)] - struct ShufflePlaylistSvc(pub Arc); + struct MoveFolderSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for ShufflePlaylistSvc { - type Response = super::ShufflePlaylistResponse; + > tonic::server::UnaryService + for MoveFolderSvc { + type Response = super::MoveFolderResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::shuffle_playlist(&inner, request) - .await + ::move_folder(&inner, request).await }; Box::pin(fut) } @@ -6139,7 +7132,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = ShufflePlaylistSvc(inner); + let method = MoveFolderSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( diff --git a/cli/src/api/rockbox_descriptor.bin b/cli/src/api/rockbox_descriptor.bin index f9e87edaf41..e926426017f 100644 Binary files a/cli/src/api/rockbox_descriptor.bin and b/cli/src/api/rockbox_descriptor.bin differ diff --git a/crates/graphql/src/schema/objects/folder.rs b/crates/graphql/src/schema/objects/folder.rs new file mode 100644 index 00000000000..57ed7642d53 --- /dev/null +++ b/crates/graphql/src/schema/objects/folder.rs @@ -0,0 +1,34 @@ +use async_graphql::*; +use serde::{Deserialize, Serialize}; + +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct Folder { + pub id: String, + pub name: String, + pub parent_id: Option, + pub created_at: String, + pub updated_at: String, +} + +#[Object] +impl Folder { + async fn id(&self) -> &str { + &self.id + } + + async fn name(&self) -> &str { + &self.name + } + + async fn parent_id(&self) -> Option { + self.parent_id.clone() + } + + async fn created_at(&self) -> &str { + &self.created_at + } + + async fn updated_at(&self) -> &str { + &self.updated_at + } +} diff --git a/crates/graphql/src/schema/objects/mod.rs b/crates/graphql/src/schema/objects/mod.rs index dbf17f2efcc..7b6a1d6d261 100644 --- a/crates/graphql/src/schema/objects/mod.rs +++ b/crates/graphql/src/schema/objects/mod.rs @@ -5,6 +5,7 @@ pub mod compressor_settings; pub mod device; pub mod entry; pub mod eq_band_setting; +pub mod folder; pub mod new_global_settings; pub mod playlist; pub mod replaygain_settings; diff --git a/crates/graphql/src/schema/objects/playlist.rs b/crates/graphql/src/schema/objects/playlist.rs index efc7d1b8d90..6e7ff4272aa 100644 --- a/crates/graphql/src/schema/objects/playlist.rs +++ b/crates/graphql/src/schema/objects/playlist.rs @@ -12,6 +12,13 @@ pub struct Playlist { pub seed: i32, pub last_shuffled_start: i32, pub tracks: Vec, + pub folder_id: Option, + pub name: Option, + pub created_at: Option, + pub updated_at: Option, + pub description: Option, + pub image: Option, + pub id: Option, } #[Object] @@ -47,4 +54,32 @@ impl Playlist { async fn tracks(&self) -> &Vec { &self.tracks } + + async fn folder_id(&self) -> Option { + self.folder_id.clone() + } + + async fn name(&self) -> Option { + self.name.clone() + } + + async fn created_at(&self) -> Option { + self.created_at.clone() + } + + async fn updated_at(&self) -> Option { + self.updated_at.clone() + } + + async fn description(&self) -> Option { + self.description.clone() + } + + async fn image(&self) -> Option { + self.image.clone() + } + + async fn id(&self) -> Option { + self.id.clone() + } } diff --git a/crates/graphql/src/schema/objects/track.rs b/crates/graphql/src/schema/objects/track.rs index 65aae9d810d..1afc774a6d3 100644 --- a/crates/graphql/src/schema/objects/track.rs +++ b/crates/graphql/src/schema/objects/track.rs @@ -39,8 +39,8 @@ pub struct Track { #[Object] impl Track { - async fn id(&self) -> Option<&str> { - self.id.as_deref() + async fn id(&self) -> Option { + self.id.clone() } async fn title(&self) -> &str { @@ -146,6 +146,7 @@ impl Track { impl From for Track { fn from(mp3entry: Mp3Entry) -> Self { + let id = mp3entry.id; let title = mp3entry.title; let artist = mp3entry.artist; let album = mp3entry.album; @@ -173,6 +174,7 @@ impl From for Track { let album_art = mp3entry.album_art; Track { + id, title, artist, album, diff --git a/crates/graphql/src/schema/playlist.rs b/crates/graphql/src/schema/playlist.rs index 73dc7b92e92..9e62bbd5c72 100644 --- a/crates/graphql/src/schema/playlist.rs +++ b/crates/graphql/src/schema/playlist.rs @@ -7,9 +7,11 @@ use rockbox_sys::{ events::RockboxCommand, types::{playlist_amount::PlaylistAmount, playlist_info::PlaylistInfo}, }; +use sqlx::{Pool, Sqlite}; use crate::{ - rockbox_url, schema::objects::playlist::Playlist, simplebroker::SimpleBroker, types::StatusCode, + rockbox_url, schema::objects::folder::Folder, schema::objects::playlist::Playlist, + simplebroker::SimpleBroker, types::StatusCode, }; #[derive(Default)] @@ -31,6 +33,7 @@ impl PlaylistQuery { seed: response.seed, last_shuffled_start: response.last_shuffled_start, tracks: response.entries.into_iter().map(|t| t.into()).collect(), + ..Default::default() }) } @@ -57,6 +60,85 @@ impl PlaylistQuery { let response = response.json::().await?; Ok(response.amount) } + + async fn playlist(&self, _ctx: &Context<'_>, id: String) -> Result { + let url = format!("{}/playlists/{}", rockbox_url(), id); + let client = reqwest::Client::new(); + let response = client.get(url).send().await?; + let response = response.json::().await?; + Ok(Playlist { + amount: response.amount, + index: response.index, + max_playlist_size: response.max_playlist_size, + first_index: response.first_index, + last_insert_pos: response.last_insert_pos, + seed: response.seed, + last_shuffled_start: response.last_shuffled_start, + tracks: response.entries.into_iter().map(|t| t.into()).collect(), + name: response.name, + description: response.description, + image: response.image, + created_at: response.created_at, + updated_at: response.updated_at, + id: response.id, + ..Default::default() + }) + } + + async fn playlists( + &self, + ctx: &Context<'_>, + folder_id: Option, + ) -> Result, Error> { + let pool = ctx.data::>()?; + let playlists = repo::playlist::find_by_folder(pool.clone(), folder_id).await?; + Ok(playlists + .into_iter() + .map(|p| Playlist { + id: Some(p.id), + name: Some(p.name), + folder_id: p.folder_id, + description: p.description, + image: p.image, + created_at: Some(p.created_at.to_rfc3339()), + updated_at: Some(p.updated_at.to_rfc3339()), + ..Default::default() + }) + .collect()) + } + + async fn folder(&self, ctx: &Context<'_>, id: String) -> Result, Error> { + let pool = ctx.data::>()?; + let folder = repo::folder::find(pool.clone(), &id).await?; + + Ok(folder.map(|f| Folder { + id: f.id, + name: f.name, + parent_id: f.parent_id, + created_at: f.created_at.to_rfc3339(), + updated_at: f.updated_at.to_rfc3339(), + })) + } + + async fn folders( + &self, + ctx: &Context<'_>, + parent_id: Option, + ) -> Result, Error> { + let pool = ctx.data::>()?; + let folders = repo::folder::find_by_parent(pool.clone(), parent_id).await?; + let folders = folders + .into_iter() + .map(|f| Folder { + id: f.id, + name: f.name, + parent_id: f.parent_id, + created_at: f.created_at.to_rfc3339(), + updated_at: f.updated_at.to_rfc3339(), + }) + .collect(); + Ok(folders) + } } #[derive(Default)] @@ -117,9 +199,15 @@ impl PlaylistMutation { Ok(0) } - async fn playlist_remove_track(&self, ctx: &Context<'_>, index: i32) -> Result { + async fn playlist_remove_track( + &self, + ctx: &Context<'_>, + index: i32, + playlist_id: Option, + ) -> Result { let client = ctx.data::().unwrap(); - let url = format!("{}/playlists/current/tracks", rockbox_url()); + let playlist_id = playlist_id.unwrap_or("current".to_string()); + let url = format!("{}/playlists/{}/tracks", rockbox_url(), playlist_id); let body = serde_json::json!({ "positions": vec![index], }); @@ -131,12 +219,17 @@ impl PlaylistMutation { "playlist sync".to_string() } - async fn playlist_remove_all_tracks(&self, ctx: &Context<'_>) -> Result { + async fn playlist_remove_all_tracks( + &self, + ctx: &Context<'_>, + playlist_id: Option, + ) -> Result { let client = ctx.data::().unwrap(); let body = serde_json::json!({ "positions": [], }); - let url = format!("{}/playlists/current/tracks", rockbox_url()); + let playlist_id = playlist_id.unwrap_or("current".to_string()); + let url = format!("{}/playlists/{}/tracks", rockbox_url(), playlist_id); let response = client.delete(&url).json(&body).send().await?; let start_index = response.text().await?.parse()?; Ok(start_index) @@ -163,7 +256,7 @@ impl PlaylistMutation { async fn insert_tracks( &self, ctx: &Context<'_>, - _playlist_id: Option, + playlist_id: Option, position: i32, tracks: Vec, ) -> Result { @@ -172,7 +265,8 @@ impl PlaylistMutation { "position": position, "tracks": tracks, }); - let url = format!("{}/playlists/current/tracks", rockbox_url()); + let playlist_id = playlist_id.unwrap_or("current".to_string()); + let url = format!("{}/playlists/{}/tracks", rockbox_url(), playlist_id); let response = client.post(&url).json(&body).send().await?; let start_index = response.text().await?.parse()?; Ok(start_index) @@ -181,7 +275,7 @@ impl PlaylistMutation { async fn insert_directory( &self, ctx: &Context<'_>, - _playlist_id: Option, + playlist_id: Option, position: i32, directory: String, ) -> Result { @@ -191,7 +285,8 @@ impl PlaylistMutation { "tracks": [], "directory": directory, }); - let url = format!("{}/playlists/current/tracks", rockbox_url()); + let playlist_id = playlist_id.unwrap_or("current".to_string()); + let url = format!("{}/playlists/{}/tracks", rockbox_url(), playlist_id); let response = client.post(&url).json(&body).send().await?; let start_index = response.text().await?.parse()?; Ok(start_index) @@ -213,6 +308,7 @@ impl PlaylistMutation { ctx: &Context<'_>, album_id: String, position: i32, + playlist_id: Option, ) -> Result { let client = ctx.data::().unwrap(); let pool = ctx.data::>()?; @@ -222,7 +318,8 @@ impl PlaylistMutation { "position": position, "tracks": tracks, }); - let url = format!("{}/playlists/current/tracks", rockbox_url()); + let playlist_id = playlist_id.unwrap_or("current".to_string()); + let url = format!("{}/playlists/{}/tracks", rockbox_url(), playlist_id); let response = client.post(&url).json(&body).send().await?; let start_index = response.text().await?.parse()?; Ok(start_index) @@ -235,6 +332,106 @@ impl PlaylistMutation { let ret = response.text().await?.parse()?; Ok(ret) } + + async fn create_folder( + &self, + _ctx: &Context<'_>, + name: String, + parent_id: Option, + ) -> Result { + let url = format!("{}/folders", rockbox_url()); + let body = match parent_id { + Some(parent_id) => serde_json::json!({ + "name": name, + "parent_id": parent_id, + }), + None => serde_json::json!({ + "name": name, + }), + }; + let client = reqwest::Client::new(); + let response = client.post(&url).json(&body).send().await?; + let response = response.json::().await?; + Ok(response) + } + + async fn remove_folder(&self, _ctx: &Context<'_>, id: String) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/folders/{}", rockbox_url(), id); + client.delete(&url).send().await?; + Ok(id) + } + + async fn remove_playlist(&self, _ctx: &Context<'_>, id: String) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/playlists/{}", rockbox_url(), id); + client.delete(&url).send().await?; + Ok(id) + } + + async fn rename_folder( + &self, + _ctx: &Context<'_>, + id: String, + name: String, + ) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/folders/{}", rockbox_url(), id); + client + .put(&url) + .json(&serde_json::json!({"name": name})) + .send() + .await?; + Ok(id) + } + + async fn rename_playlist( + &self, + _ctx: &Context<'_>, + id: String, + name: String, + ) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/playlists/{}", rockbox_url(), id); + client + .put(&url) + .json(&serde_json::json!({"name": name})) + .send() + .await?; + Ok(id) + } + + async fn move_folder( + &self, + _ctx: &Context<'_>, + folder_id: String, + destination: String, + ) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/folders/{}", rockbox_url(), folder_id); + client + .put(&url) + .json(&serde_json::json!({"parent_id": destination})) + .send() + .await?; + Ok(folder_id) + } + + async fn move_playlist( + &self, + _ctx: &Context<'_>, + playlist_id: String, + destination: String, + ) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/playlists/{}", rockbox_url(), playlist_id); + client + .put(&url) + .json(&serde_json::json!({"folder_id": destination})) + .send() + .await?; + Ok(playlist_id) + } } #[derive(Default)] diff --git a/crates/library/migrations/20241227044632_add-playlist-track-position.sql b/crates/library/migrations/20241227044632_add-playlist-track-position.sql new file mode 100644 index 00000000000..063e7872b49 --- /dev/null +++ b/crates/library/migrations/20241227044632_add-playlist-track-position.sql @@ -0,0 +1,2 @@ +-- Add migration script here +ALTER TABLE playlist_tracks ADD COLUMN position INT NOT NULL DEFAULT 0; diff --git a/crates/library/src/entity/playlist_tracks.rs b/crates/library/src/entity/playlist_tracks.rs index 201e28cb9e2..f91729dffbe 100644 --- a/crates/library/src/entity/playlist_tracks.rs +++ b/crates/library/src/entity/playlist_tracks.rs @@ -1,6 +1,12 @@ -#[derive(sqlx::FromRow, Default)] +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(sqlx::FromRow, Default, Serialize, Deserialize)] pub struct PlaylistTracks { pub id: String, pub playlist_id: String, pub track_id: String, + pub position: u32, + #[serde(with = "chrono::serde::ts_seconds")] + pub created_at: DateTime, } diff --git a/crates/library/src/lib.rs b/crates/library/src/lib.rs index 44368d8e929..189c00feac9 100644 --- a/crates/library/src/lib.rs +++ b/crates/library/src/lib.rs @@ -43,6 +43,16 @@ pub async fn create_connection_pool() -> Result, Error> { Err(_) => println!("album_id column already exists"), } + match pool + .execute(include_str!( + "../migrations/20241227044632_add-playlist-track-position.sql" + )) + .await + { + Ok(_) => {} + Err(_) => println!("position column already exists"), + } + sqlx::query("PRAGMA journal_mode=WAL") .execute(&pool) .await?; diff --git a/crates/library/src/repo/folder.rs b/crates/library/src/repo/folder.rs index 9a7b59ae0e8..5d22deac944 100644 --- a/crates/library/src/repo/folder.rs +++ b/crates/library/src/repo/folder.rs @@ -1,8 +1,77 @@ use crate::entity::folder::Folder; -use sqlx::{Pool, Sqlite}; +use sqlx::{types::chrono, Pool, Sqlite}; -pub async fn save(pool: Pool, folder: Folder) {} +pub async fn save(pool: Pool, folder: Folder) -> Result { + sqlx::query( + r#" + INSERT INTO folder ( + id, + name, + created_at, + updated_at + ) + VALUES ($1, $2, $3, $4) + "#, + ) + .bind(&folder.id) + .bind(&folder.name) + .bind(folder.created_at) + .bind(folder.updated_at) + .execute(&pool) + .await?; + Ok(folder.id) +} -pub async fn find(pool: Pool) {} +pub async fn find(pool: Pool, id: &str) -> Result, sqlx::Error> { + sqlx::query_as::<_, Folder>(r#"SELECT * FROM folder WHERE id = $1"#) + .bind(id) + .fetch_optional(&pool) + .await +} -pub async fn all(pool: Pool) {} +pub async fn find_by_parent( + pool: Pool, + parent_id: Option, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, Folder>(r#"SELECT * FROM folder WHERE parent_id IS $1 ORDER BY name ASC"#) + .bind(parent_id) + .fetch_all(&pool) + .await +} + +pub async fn all(pool: Pool) -> Result, sqlx::Error> { + sqlx::query_as::<_, Folder>(r#"SELECT * FROM folder ORDER BY name ASC"#) + .fetch_all(&pool) + .await +} + +pub async fn delete(pool: Pool, id: &str) -> Result<(), sqlx::Error> { + sqlx::query(r#"DELETE FROM folder WHERE id = $1"#) + .bind(id) + .execute(&pool) + .await?; + Ok(()) +} + +pub async fn update(pool: Pool, folder: Folder) -> Result<(), sqlx::Error> { + let name = match folder.name.is_empty() { + true => None, + false => Some(&folder.name), + }; + sqlx::query( + r#" + UPDATE folder SET + name = $2, + updated_at = $3, + parent_id = $4 + WHERE id = $1 + "#, + ) + .bind(&folder.id) + .bind(name) + .bind(chrono::Utc::now()) + .bind(&folder.parent_id) + .execute(&pool) + .await?; + Ok(()) +} diff --git a/crates/library/src/repo/playlist.rs b/crates/library/src/repo/playlist.rs index 46aa76dcbf1..56f9a638e45 100644 --- a/crates/library/src/repo/playlist.rs +++ b/crates/library/src/repo/playlist.rs @@ -1,8 +1,91 @@ use crate::entity::playlist::Playlist; use sqlx::{Pool, Sqlite}; -pub async fn save(pool: Pool, playlist: Playlist) {} +pub async fn save(pool: Pool, playlist: Playlist) -> Result { + sqlx::query( + r#" + INSERT INTO playlist ( + id, + name, + image, + description, + folder_id, + created_at, + updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + "#, + ) + .bind(&playlist.id) + .bind(&playlist.name) + .bind(&playlist.image) + .bind(&playlist.description) + .bind(&playlist.folder_id) + .bind(playlist.created_at) + .bind(playlist.updated_at) + .execute(&pool) + .await?; + Ok(playlist.id) +} -pub async fn find(pool: Pool) {} +pub async fn find(pool: Pool, id: &str) -> Result, sqlx::Error> { + sqlx::query_as::<_, Playlist>(r#"SELECT * FROM playlist WHERE id = $1"#) + .bind(id) + .fetch_optional(&pool) + .await +} -pub async fn all(pool: Pool) {} +pub async fn find_by_folder( + pool: Pool, + folder_id: Option, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, Playlist>( + r#" + SELECT * FROM playlist WHERE folder_id IS $1 ORDER BY name ASC + "#, + ) + .bind(folder_id) + .fetch_all(&pool) + .await +} + +pub async fn all(pool: Pool) -> Result, sqlx::Error> { + sqlx::query_as::<_, Playlist>(r#"SELECT * FROM playlist ORDER BY name ASC"#) + .fetch_all(&pool) + .await +} + +pub async fn delete(pool: Pool, id: &str) -> Result<(), sqlx::Error> { + sqlx::query(r#"DELETE FROM playlist WHERE id = $1"#) + .bind(id) + .execute(&pool) + .await?; + Ok(()) +} + +pub async fn update(pool: Pool, playlist: Playlist) -> Result<(), sqlx::Error> { + let name = match playlist.name.is_empty() { + true => None, + false => Some(&playlist.name), + }; + sqlx::query( + r#" + UPDATE playlist SET + name = $2, + description = $3, + image = $4, + updated_at = $5, + folder_id = $6 + WHERE id = $1 + "#, + ) + .bind(&playlist.id) + .bind(name) + .bind(playlist.description) + .bind(playlist.image) + .bind(chrono::Utc::now()) + .bind(&playlist.folder_id) + .execute(&pool) + .await?; + Ok(()) +} diff --git a/crates/library/src/repo/playlist_tracks.rs b/crates/library/src/repo/playlist_tracks.rs index 877972430eb..e12564c12a3 100644 --- a/crates/library/src/repo/playlist_tracks.rs +++ b/crates/library/src/repo/playlist_tracks.rs @@ -1,6 +1,102 @@ -use crate::entity::playlist_tracks::PlaylistTracks; +use crate::entity::{playlist_tracks::PlaylistTracks, track::Track}; use sqlx::{Pool, Sqlite}; -pub async fn save(pool: Pool, playlist_track: PlaylistTracks) {} +pub async fn save(pool: Pool, playlist_track: PlaylistTracks) -> Result<(), sqlx::Error> { + sqlx::query( + r#" + INSERT INTO playlist_tracks ( + id, + playlist_id, + track_id, + position, + created_at + ) + VALUES ($1, $2, $3, $4, $5) + "#, + ) + .bind(&playlist_track.id) + .bind(&playlist_track.playlist_id) + .bind(&playlist_track.track_id) + .bind(playlist_track.position) + .bind(playlist_track.created_at) + .execute(&pool) + .await?; + Ok(()) +} -pub async fn find(pool: Pool) {} +pub async fn find_by_playlist( + pool: Pool, + playlist_id: &str, +) -> Result, sqlx::Error> { + match sqlx::query_as::<_, Track>( + r#" + SELECT * FROM playlist_tracks + LEFT JOIN track ON playlist_tracks.track_id = track.id + WHERE playlist_tracks.playlist_id = $1 + ORDER BY playlist_tracks.position ASC + "#, + ) + .bind(playlist_id) + .fetch_all(&pool) + .await + { + Ok(playlist_tracks) => Ok(playlist_tracks), + Err(e) => { + eprintln!("Error finding playlist tracks: {:?}", e); + Err(e) + } + } +} + +pub async fn delete(pool: Pool, id: &str) -> Result<(), sqlx::Error> { + sqlx::query(r#"DELETE FROM playlist_tracks WHERE id = $1"#) + .bind(id) + .execute(&pool) + .await?; + Ok(()) +} + +pub async fn delete_by_playlist(pool: Pool, playlist_id: &str) -> Result<(), sqlx::Error> { + sqlx::query(r#"DELETE FROM playlist_tracks WHERE playlist_id = $1"#) + .bind(playlist_id) + .execute(&pool) + .await?; + Ok(()) +} + +pub async fn delete_track_at( + pool: Pool, + playlist_id: &str, + position: u32, +) -> Result<(), sqlx::Error> { + sqlx::query(r#"DELETE FROM playlist_tracks WHERE playlist_id = $1 AND position = $2"#) + .bind(playlist_id) + .bind(position) + .execute(&pool) + .await?; + + let tracks = sqlx::query_as::<_, Track>( + r#" + SELECT * FROM playlist_tracks + LEFT JOIN track ON playlist_tracks.track_id = track.id + WHERE playlist_tracks.playlist_id = $1 + ORDER BY playlist_tracks.created_at ASC + "#, + ) + .bind(playlist_id) + .fetch_all(&pool) + .await?; + + for (i, track) in tracks.iter().enumerate() { + sqlx::query( + r#"UPDATE playlist_tracks SET position = $1 WHERE playlist_id = $2 AND track_id = $3"#, + ) + .bind(i as u32) + .bind(playlist_id) + .bind(&track.id) + .execute(&pool) + .await?; + } + + Ok(()) +} diff --git a/crates/mpd/src/handlers/queue.rs b/crates/mpd/src/handlers/queue.rs index b67908703ca..7af7c3817ec 100644 --- a/crates/mpd/src/handlers/queue.rs +++ b/crates/mpd/src/handlers/queue.rs @@ -284,7 +284,10 @@ pub async fn handle_deleteid( } }; ctx.playlist - .remove_tracks(RemoveTracksRequest { positions }) + .remove_tracks(RemoveTracksRequest { + positions, + playlist_id: None, + }) .await?; if !ctx.batch { tx.send("OK\n".to_string()).await?; @@ -318,7 +321,10 @@ pub async fn handle_delete( let range: Vec = arg.split(':').map(|x| x.parse::().unwrap()).collect(); let positions: Vec = (range[0]..=range[1]).collect(); ctx.playlist - .remove_tracks(RemoveTracksRequest { positions }) + .remove_tracks(RemoveTracksRequest { + positions, + playlist_id: None, + }) .await?; if !ctx.batch { tx.send("OK\n".to_string()).await?; @@ -336,7 +342,10 @@ pub async fn handle_delete( } }; ctx.playlist - .remove_tracks(RemoveTracksRequest { positions }) + .remove_tracks(RemoveTracksRequest { + positions, + playlist_id: None, + }) .await?; if !ctx.batch { tx.send("OK\n".to_string()).await?; @@ -356,7 +365,7 @@ pub async fn handle_clear( tx: Sender, ) -> Result { ctx.playlist - .remove_all_tracks(RemoveAllTracksRequest {}) + .remove_all_tracks(RemoveAllTracksRequest { playlist_id: None }) .await?; if !ctx.batch { tx.send("OK\n".to_string()).await?; diff --git a/crates/rpc/proto/rockbox/v1alpha1/playlist.proto b/crates/rpc/proto/rockbox/v1alpha1/playlist.proto index 20e5674ea17..70394430b15 100644 --- a/crates/rpc/proto/rockbox/v1alpha1/playlist.proto +++ b/crates/rpc/proto/rockbox/v1alpha1/playlist.proto @@ -66,16 +66,19 @@ message SyncRequest {} message SyncResponse {} -message RemoveAllTracksRequest {} +message RemoveAllTracksRequest { optional string playlist_id = 1; } message RemoveAllTracksResponse {} -message RemoveTracksRequest { repeated int32 positions = 1; } +message RemoveTracksRequest { + repeated int32 positions = 1; + optional string playlist_id = 2; +} message RemoveTracksResponse {} message CreatePlaylistRequest { - string name = 1; + optional string name = 1; repeated string tracks = 2; optional string folder_id = 3; } @@ -114,6 +117,7 @@ message InsertAlbumRequest { int32 position = 1; string album_id = 2; optional bool shuffle = 3; + optional string playlist_id = 4; } message InsertAlbumResponse {} @@ -122,6 +126,7 @@ message InsertArtistTracksRequest { int32 position = 1; string artist_id = 2; optional bool shuffle = 3; + optional string playlist_id = 4; } message InsertArtistTracksResponse {} @@ -130,6 +135,83 @@ message ShufflePlaylistRequest { int32 start_index = 1; } message ShufflePlaylistResponse {} +message GetPlaylistRequest { string playlist_id = 1; } + +message GetPlaylistResponse { + string id = 1; + string name = 2; + optional string folder_id = 3; + optional string image = 4; + optional string description = 5; + int32 amount = 6; + string created_at = 7; + string updated_at = 8; + repeated rockbox.v1alpha1.CurrentTrackResponse tracks = 9; +} + +message CreateFolderRequest { + string name = 1; + optional string parent_id = 2; +} + +message CreateFolderResponse { string folder_id = 1; } + +message GetFolderRequest { string id = 1; } + +message GetFolderResponse { + string id = 1; + string name = 2; + optional string parent_id = 3; +} + +message GetFoldersRequest { optional string parent_id = 1; } + +message GetFoldersResponse { + repeated rockbox.v1alpha1.GetFolderResponse folders = 1; +} + +message RemoveFolderRequest { string id = 1; } + +message RemoveFolderResponse {} + +message RemovePlaylistRequest { string id = 1; } + +message RemovePlaylistResponse {} + +message RenamePlaylistRequest { + string id = 1; + string name = 2; +} + +message RenamePlaylistResponse {} + +message RenameFolderRequest { + string id = 1; + string name = 2; +} + +message RenameFolderResponse {} + +message MovePlaylistRequest { + string playlist_id = 1; + string folder_id = 2; +} + +message MovePlaylistResponse {} + +message MoveFolderRequest { + string folder_id = 1; + string parent_id = 2; +} + +message MoveFolderResponse {} + +message GetPlaylistsRequest { optional string folder_id = 1; } + +message GetPlaylistsResponse { + repeated rockbox.v1alpha1.GetPlaylistResponse playlists = 1; +} + service PlaylistService { rpc GetCurrent(GetCurrentRequest) returns (GetCurrentResponse) {} rpc GetResumeInfo(GetResumeInfoRequest) returns (GetResumeInfoResponse) {} @@ -156,4 +238,15 @@ service PlaylistService { returns (InsertArtistTracksResponse) {} rpc ShufflePlaylist(ShufflePlaylistRequest) returns (ShufflePlaylistResponse) {} + rpc GetPlaylist(GetPlaylistRequest) returns (GetPlaylistResponse) {} + rpc GetPlaylists(GetPlaylistsRequest) returns (GetPlaylistsResponse) {} + rpc CreateFolder(CreateFolderRequest) returns (CreateFolderResponse) {} + rpc GetFolder(GetFolderRequest) returns (GetFolderResponse) {} + rpc GetFolders(GetFoldersRequest) returns (GetFoldersResponse) {} + rpc RemoveFolder(RemoveFolderRequest) returns (RemoveFolderResponse) {} + rpc RemovePlaylist(RemovePlaylistRequest) returns (RemovePlaylistResponse) {} + rpc RenamePlaylist(RenamePlaylistRequest) returns (RenamePlaylistResponse) {} + rpc RenameFolder(RenameFolderRequest) returns (RenameFolderResponse) {} + rpc MovePlaylist(MovePlaylistRequest) returns (MovePlaylistResponse) {} + rpc MoveFolder(MoveFolderRequest) returns (MoveFolderResponse) {} } diff --git a/crates/rpc/src/api/rockbox.v1alpha1.rs b/crates/rpc/src/api/rockbox.v1alpha1.rs index b6a3b22ee13..6ee8d4399ff 100644 --- a/crates/rpc/src/api/rockbox.v1alpha1.rs +++ b/crates/rpc/src/api/rockbox.v1alpha1.rs @@ -4910,21 +4910,26 @@ pub struct StartResponse {} pub struct SyncRequest {} #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct SyncResponse {} -#[derive(Clone, Copy, PartialEq, ::prost::Message)] -pub struct RemoveAllTracksRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveAllTracksRequest { + #[prost(string, optional, tag = "1")] + pub playlist_id: ::core::option::Option<::prost::alloc::string::String>, +} #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct RemoveAllTracksResponse {} #[derive(Clone, PartialEq, ::prost::Message)] pub struct RemoveTracksRequest { #[prost(int32, repeated, tag = "1")] pub positions: ::prost::alloc::vec::Vec, + #[prost(string, optional, tag = "2")] + pub playlist_id: ::core::option::Option<::prost::alloc::string::String>, } #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct RemoveTracksResponse {} #[derive(Clone, PartialEq, ::prost::Message)] pub struct CreatePlaylistRequest { - #[prost(string, tag = "1")] - pub name: ::prost::alloc::string::String, + #[prost(string, optional, tag = "1")] + pub name: ::core::option::Option<::prost::alloc::string::String>, #[prost(string, repeated, tag = "2")] pub tracks: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(string, optional, tag = "3")] @@ -4984,6 +4989,8 @@ pub struct InsertAlbumRequest { pub album_id: ::prost::alloc::string::String, #[prost(bool, optional, tag = "3")] pub shuffle: ::core::option::Option, + #[prost(string, optional, tag = "4")] + pub playlist_id: ::core::option::Option<::prost::alloc::string::String>, } #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct InsertAlbumResponse {} @@ -4995,6 +5002,8 @@ pub struct InsertArtistTracksRequest { pub artist_id: ::prost::alloc::string::String, #[prost(bool, optional, tag = "3")] pub shuffle: ::core::option::Option, + #[prost(string, optional, tag = "4")] + pub playlist_id: ::core::option::Option<::prost::alloc::string::String>, } #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct InsertArtistTracksResponse {} @@ -5005,6 +5014,128 @@ pub struct ShufflePlaylistRequest { } #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct ShufflePlaylistResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPlaylistRequest { + #[prost(string, tag = "1")] + pub playlist_id: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPlaylistResponse { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, + #[prost(string, optional, tag = "3")] + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "4")] + pub image: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "5")] + pub description: ::core::option::Option<::prost::alloc::string::String>, + #[prost(int32, tag = "6")] + pub amount: i32, + #[prost(string, tag = "7")] + pub created_at: ::prost::alloc::string::String, + #[prost(string, tag = "8")] + pub updated_at: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "9")] + pub tracks: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CreateFolderRequest { + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + #[prost(string, optional, tag = "2")] + pub parent_id: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CreateFolderResponse { + #[prost(string, tag = "1")] + pub folder_id: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetFolderRequest { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetFolderResponse { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, + #[prost(string, optional, tag = "3")] + pub parent_id: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetFoldersRequest { + #[prost(string, optional, tag = "1")] + pub parent_id: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetFoldersResponse { + #[prost(message, repeated, tag = "1")] + pub folders: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveFolderRequest { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct RemoveFolderResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemovePlaylistRequest { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct RemovePlaylistResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RenamePlaylistRequest { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct RenamePlaylistResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RenameFolderRequest { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct RenameFolderResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MovePlaylistRequest { + #[prost(string, tag = "1")] + pub playlist_id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub folder_id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct MovePlaylistResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MoveFolderRequest { + #[prost(string, tag = "1")] + pub folder_id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub parent_id: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct MoveFolderResponse {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPlaylistsRequest { + #[prost(string, optional, tag = "1")] + pub folder_id: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPlaylistsResponse { + #[prost(message, repeated, tag = "1")] + pub playlists: ::prost::alloc::vec::Vec, +} /// Generated client implementations. pub mod playlist_service_client { #![allow( @@ -5616,77 +5747,363 @@ pub mod playlist_service_client { ); self.inner.unary(req, path, codec).await } - } -} -/// Generated server implementations. -pub mod playlist_service_server { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tonic::codegen::*; - /// Generated trait containing gRPC methods that should be implemented for use with PlaylistServiceServer. - #[async_trait] - pub trait PlaylistService: std::marker::Send + std::marker::Sync + 'static { - async fn get_current( - &self, - request: tonic::Request, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - >; - async fn get_resume_info( - &self, - request: tonic::Request, + pub async fn get_playlist( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn get_track_info( - &self, - request: tonic::Request, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/GetPlaylist", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "GetPlaylist"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn get_playlists( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn get_first_index( - &self, - request: tonic::Request, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/GetPlaylists", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "GetPlaylists"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn create_folder( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn get_display_index( - &self, - request: tonic::Request, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/CreateFolder", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "CreateFolder"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn get_folder( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn amount( - &self, - request: tonic::Request, - ) -> std::result::Result, tonic::Status>; - async fn playlist_resume( - &self, - request: tonic::Request, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/GetFolder", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "GetFolder"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn get_folders( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn resume_track( - &self, - request: tonic::Request, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/GetFolders", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "GetFolders"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn remove_folder( + &mut self, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response, + tonic::Response, tonic::Status, - >; - async fn set_modified( - &self, - request: tonic::Request, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/RemoveFolder", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "RemoveFolder"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn remove_playlist( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/RemovePlaylist", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "RemovePlaylist"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn rename_playlist( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/RenamePlaylist", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "RenamePlaylist"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn rename_folder( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/RenameFolder", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "RenameFolder"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn move_playlist( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/MovePlaylist", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "MovePlaylist"), + ); + self.inner.unary(req, path, codec).await + } + pub async fn move_folder( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rockbox.v1alpha1.PlaylistService/MoveFolder", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("rockbox.v1alpha1.PlaylistService", "MoveFolder"), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod playlist_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with PlaylistServiceServer. + #[async_trait] + pub trait PlaylistService: std::marker::Send + std::marker::Sync + 'static { + async fn get_current( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_resume_info( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_track_info( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_first_index( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_display_index( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn amount( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn playlist_resume( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn resume_track( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn set_modified( + &self, + request: tonic::Request, ) -> std::result::Result< tonic::Response, tonic::Status, @@ -5762,6 +6179,83 @@ pub mod playlist_service_server { tonic::Response, tonic::Status, >; + async fn get_playlist( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_playlists( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn create_folder( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_folder( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_folders( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn remove_folder( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn remove_playlist( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn rename_playlist( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn rename_folder( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn move_playlist( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn move_folder( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } #[derive(Debug)] pub struct PlaylistServiceServer { @@ -5841,23 +6335,522 @@ pub mod playlist_service_server { match req.uri().path() { "/rockbox.v1alpha1.PlaylistService/GetCurrent" => { #[allow(non_camel_case_types)] - struct GetCurrentSvc(pub Arc); + struct GetCurrentSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for GetCurrentSvc { + type Response = super::GetCurrentResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_current(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetCurrentSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/GetResumeInfo" => { + #[allow(non_camel_case_types)] + struct GetResumeInfoSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for GetResumeInfoSvc { + type Response = super::GetResumeInfoResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_resume_info(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetResumeInfoSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/GetTrackInfo" => { + #[allow(non_camel_case_types)] + struct GetTrackInfoSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for GetTrackInfoSvc { + type Response = super::GetTrackInfoResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_track_info(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetTrackInfoSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/GetFirstIndex" => { + #[allow(non_camel_case_types)] + struct GetFirstIndexSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for GetFirstIndexSvc { + type Response = super::GetFirstIndexResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_first_index(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetFirstIndexSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/GetDisplayIndex" => { + #[allow(non_camel_case_types)] + struct GetDisplayIndexSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for GetDisplayIndexSvc { + type Response = super::GetDisplayIndexResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_display_index(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetDisplayIndexSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/Amount" => { + #[allow(non_camel_case_types)] + struct AmountSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for AmountSvc { + type Response = super::AmountResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::amount(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = AmountSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/PlaylistResume" => { + #[allow(non_camel_case_types)] + struct PlaylistResumeSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for PlaylistResumeSvc { + type Response = super::PlaylistResumeResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::playlist_resume(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = PlaylistResumeSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/ResumeTrack" => { + #[allow(non_camel_case_types)] + struct ResumeTrackSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for ResumeTrackSvc { + type Response = super::ResumeTrackResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::resume_track(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ResumeTrackSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/SetModified" => { + #[allow(non_camel_case_types)] + struct SetModifiedSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService + for SetModifiedSvc { + type Response = super::SetModifiedResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::set_modified(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SetModifiedSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/Start" => { + #[allow(non_camel_case_types)] + struct StartSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService for StartSvc { + type Response = super::StartResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::start(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = StartSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/Sync" => { + #[allow(non_camel_case_types)] + struct SyncSvc(pub Arc); + impl< + T: PlaylistService, + > tonic::server::UnaryService for SyncSvc { + type Response = super::SyncResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::sync(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SyncSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/rockbox.v1alpha1.PlaylistService/RemoveAllTracks" => { + #[allow(non_camel_case_types)] + struct RemoveAllTracksSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for GetCurrentSvc { - type Response = super::GetCurrentResponse; + > tonic::server::UnaryService + for RemoveAllTracksSvc { + type Response = super::RemoveAllTracksResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_current(&inner, request).await + ::remove_all_tracks(&inner, request) + .await }; Box::pin(fut) } @@ -5868,7 +6861,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetCurrentSvc(inner); + let method = RemoveAllTracksSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5884,26 +6877,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/GetResumeInfo" => { + "/rockbox.v1alpha1.PlaylistService/RemoveTracks" => { #[allow(non_camel_case_types)] - struct GetResumeInfoSvc(pub Arc); + struct RemoveTracksSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for GetResumeInfoSvc { - type Response = super::GetResumeInfoResponse; + > tonic::server::UnaryService + for RemoveTracksSvc { + type Response = super::RemoveTracksResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_resume_info(&inner, request) - .await + ::remove_tracks(&inner, request).await }; Box::pin(fut) } @@ -5914,7 +6906,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetResumeInfoSvc(inner); + let method = RemoveTracksSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5930,25 +6922,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/GetTrackInfo" => { + "/rockbox.v1alpha1.PlaylistService/CreatePlaylist" => { #[allow(non_camel_case_types)] - struct GetTrackInfoSvc(pub Arc); + struct CreatePlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for GetTrackInfoSvc { - type Response = super::GetTrackInfoResponse; + > tonic::server::UnaryService + for CreatePlaylistSvc { + type Response = super::CreatePlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_track_info(&inner, request) + ::create_playlist(&inner, request) .await }; Box::pin(fut) @@ -5960,7 +6952,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetTrackInfoSvc(inner); + let method = CreatePlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -5976,26 +6968,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/GetFirstIndex" => { + "/rockbox.v1alpha1.PlaylistService/InsertTracks" => { #[allow(non_camel_case_types)] - struct GetFirstIndexSvc(pub Arc); + struct InsertTracksSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for GetFirstIndexSvc { - type Response = super::GetFirstIndexResponse; + > tonic::server::UnaryService + for InsertTracksSvc { + type Response = super::InsertTracksResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_first_index(&inner, request) - .await + ::insert_tracks(&inner, request).await }; Box::pin(fut) } @@ -6006,7 +6997,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetFirstIndexSvc(inner); + let method = InsertTracksSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6022,25 +7013,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/GetDisplayIndex" => { + "/rockbox.v1alpha1.PlaylistService/InsertDirectory" => { #[allow(non_camel_case_types)] - struct GetDisplayIndexSvc(pub Arc); + struct InsertDirectorySvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for GetDisplayIndexSvc { - type Response = super::GetDisplayIndexResponse; + > tonic::server::UnaryService + for InsertDirectorySvc { + type Response = super::InsertDirectoryResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::get_display_index(&inner, request) + ::insert_directory(&inner, request) .await }; Box::pin(fut) @@ -6052,7 +7043,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = GetDisplayIndexSvc(inner); + let method = InsertDirectorySvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6068,25 +7059,26 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/Amount" => { + "/rockbox.v1alpha1.PlaylistService/InsertPlaylist" => { #[allow(non_camel_case_types)] - struct AmountSvc(pub Arc); + struct InsertPlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for AmountSvc { - type Response = super::AmountResponse; + > tonic::server::UnaryService + for InsertPlaylistSvc { + type Response = super::InsertPlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::amount(&inner, request).await + ::insert_playlist(&inner, request) + .await }; Box::pin(fut) } @@ -6097,7 +7089,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = AmountSvc(inner); + let method = InsertPlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6113,26 +7105,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/PlaylistResume" => { + "/rockbox.v1alpha1.PlaylistService/InsertAlbum" => { #[allow(non_camel_case_types)] - struct PlaylistResumeSvc(pub Arc); + struct InsertAlbumSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for PlaylistResumeSvc { - type Response = super::PlaylistResumeResponse; + > tonic::server::UnaryService + for InsertAlbumSvc { + type Response = super::InsertAlbumResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::playlist_resume(&inner, request) - .await + ::insert_album(&inner, request).await }; Box::pin(fut) } @@ -6143,7 +7134,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = PlaylistResumeSvc(inner); + let method = InsertAlbumSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6159,25 +7150,29 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/ResumeTrack" => { + "/rockbox.v1alpha1.PlaylistService/InsertArtistTracks" => { #[allow(non_camel_case_types)] - struct ResumeTrackSvc(pub Arc); + struct InsertArtistTracksSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for ResumeTrackSvc { - type Response = super::ResumeTrackResponse; + > tonic::server::UnaryService + for InsertArtistTracksSvc { + type Response = super::InsertArtistTracksResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::resume_track(&inner, request).await + ::insert_artist_tracks( + &inner, + request, + ) + .await }; Box::pin(fut) } @@ -6188,7 +7183,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = ResumeTrackSvc(inner); + let method = InsertArtistTracksSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6204,25 +7199,26 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/SetModified" => { + "/rockbox.v1alpha1.PlaylistService/ShufflePlaylist" => { #[allow(non_camel_case_types)] - struct SetModifiedSvc(pub Arc); + struct ShufflePlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for SetModifiedSvc { - type Response = super::SetModifiedResponse; + > tonic::server::UnaryService + for ShufflePlaylistSvc { + type Response = super::ShufflePlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::set_modified(&inner, request).await + ::shuffle_playlist(&inner, request) + .await }; Box::pin(fut) } @@ -6233,7 +7229,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = SetModifiedSvc(inner); + let method = ShufflePlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6249,24 +7245,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/Start" => { + "/rockbox.v1alpha1.PlaylistService/GetPlaylist" => { #[allow(non_camel_case_types)] - struct StartSvc(pub Arc); + struct GetPlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService for StartSvc { - type Response = super::StartResponse; + > tonic::server::UnaryService + for GetPlaylistSvc { + type Response = super::GetPlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::start(&inner, request).await + ::get_playlist(&inner, request).await }; Box::pin(fut) } @@ -6277,7 +7274,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = StartSvc(inner); + let method = GetPlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6293,24 +7290,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/Sync" => { + "/rockbox.v1alpha1.PlaylistService/GetPlaylists" => { #[allow(non_camel_case_types)] - struct SyncSvc(pub Arc); + struct GetPlaylistsSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService for SyncSvc { - type Response = super::SyncResponse; + > tonic::server::UnaryService + for GetPlaylistsSvc { + type Response = super::GetPlaylistsResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::sync(&inner, request).await + ::get_playlists(&inner, request).await }; Box::pin(fut) } @@ -6321,7 +7319,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = SyncSvc(inner); + let method = GetPlaylistsSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6337,26 +7335,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/RemoveAllTracks" => { + "/rockbox.v1alpha1.PlaylistService/CreateFolder" => { #[allow(non_camel_case_types)] - struct RemoveAllTracksSvc(pub Arc); + struct CreateFolderSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for RemoveAllTracksSvc { - type Response = super::RemoveAllTracksResponse; + > tonic::server::UnaryService + for CreateFolderSvc { + type Response = super::CreateFolderResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::remove_all_tracks(&inner, request) - .await + ::create_folder(&inner, request).await }; Box::pin(fut) } @@ -6367,7 +7364,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = RemoveAllTracksSvc(inner); + let method = CreateFolderSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6383,25 +7380,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/RemoveTracks" => { + "/rockbox.v1alpha1.PlaylistService/GetFolder" => { #[allow(non_camel_case_types)] - struct RemoveTracksSvc(pub Arc); + struct GetFolderSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for RemoveTracksSvc { - type Response = super::RemoveTracksResponse; + > tonic::server::UnaryService + for GetFolderSvc { + type Response = super::GetFolderResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::remove_tracks(&inner, request).await + ::get_folder(&inner, request).await }; Box::pin(fut) } @@ -6412,7 +7409,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = RemoveTracksSvc(inner); + let method = GetFolderSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6428,26 +7425,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/CreatePlaylist" => { + "/rockbox.v1alpha1.PlaylistService/GetFolders" => { #[allow(non_camel_case_types)] - struct CreatePlaylistSvc(pub Arc); + struct GetFoldersSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for CreatePlaylistSvc { - type Response = super::CreatePlaylistResponse; + > tonic::server::UnaryService + for GetFoldersSvc { + type Response = super::GetFoldersResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::create_playlist(&inner, request) - .await + ::get_folders(&inner, request).await }; Box::pin(fut) } @@ -6458,7 +7454,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = CreatePlaylistSvc(inner); + let method = GetFoldersSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6474,25 +7470,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/InsertTracks" => { + "/rockbox.v1alpha1.PlaylistService/RemoveFolder" => { #[allow(non_camel_case_types)] - struct InsertTracksSvc(pub Arc); + struct RemoveFolderSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for InsertTracksSvc { - type Response = super::InsertTracksResponse; + > tonic::server::UnaryService + for RemoveFolderSvc { + type Response = super::RemoveFolderResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::insert_tracks(&inner, request).await + ::remove_folder(&inner, request).await }; Box::pin(fut) } @@ -6503,7 +7499,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = InsertTracksSvc(inner); + let method = RemoveFolderSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6519,25 +7515,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/InsertDirectory" => { + "/rockbox.v1alpha1.PlaylistService/RemovePlaylist" => { #[allow(non_camel_case_types)] - struct InsertDirectorySvc(pub Arc); + struct RemovePlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for InsertDirectorySvc { - type Response = super::InsertDirectoryResponse; + > tonic::server::UnaryService + for RemovePlaylistSvc { + type Response = super::RemovePlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::insert_directory(&inner, request) + ::remove_playlist(&inner, request) .await }; Box::pin(fut) @@ -6549,7 +7545,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = InsertDirectorySvc(inner); + let method = RemovePlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6565,25 +7561,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/InsertPlaylist" => { + "/rockbox.v1alpha1.PlaylistService/RenamePlaylist" => { #[allow(non_camel_case_types)] - struct InsertPlaylistSvc(pub Arc); + struct RenamePlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for InsertPlaylistSvc { - type Response = super::InsertPlaylistResponse; + > tonic::server::UnaryService + for RenamePlaylistSvc { + type Response = super::RenamePlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::insert_playlist(&inner, request) + ::rename_playlist(&inner, request) .await }; Box::pin(fut) @@ -6595,7 +7591,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = InsertPlaylistSvc(inner); + let method = RenamePlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6611,25 +7607,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/InsertAlbum" => { + "/rockbox.v1alpha1.PlaylistService/RenameFolder" => { #[allow(non_camel_case_types)] - struct InsertAlbumSvc(pub Arc); + struct RenameFolderSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for InsertAlbumSvc { - type Response = super::InsertAlbumResponse; + > tonic::server::UnaryService + for RenameFolderSvc { + type Response = super::RenameFolderResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::insert_album(&inner, request).await + ::rename_folder(&inner, request).await }; Box::pin(fut) } @@ -6640,7 +7636,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = InsertAlbumSvc(inner); + let method = RenameFolderSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6656,29 +7652,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/InsertArtistTracks" => { + "/rockbox.v1alpha1.PlaylistService/MovePlaylist" => { #[allow(non_camel_case_types)] - struct InsertArtistTracksSvc(pub Arc); + struct MovePlaylistSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for InsertArtistTracksSvc { - type Response = super::InsertArtistTracksResponse; + > tonic::server::UnaryService + for MovePlaylistSvc { + type Response = super::MovePlaylistResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::insert_artist_tracks( - &inner, - request, - ) - .await + ::move_playlist(&inner, request).await }; Box::pin(fut) } @@ -6689,7 +7681,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = InsertArtistTracksSvc(inner); + let method = MovePlaylistSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( @@ -6705,26 +7697,25 @@ pub mod playlist_service_server { }; Box::pin(fut) } - "/rockbox.v1alpha1.PlaylistService/ShufflePlaylist" => { + "/rockbox.v1alpha1.PlaylistService/MoveFolder" => { #[allow(non_camel_case_types)] - struct ShufflePlaylistSvc(pub Arc); + struct MoveFolderSvc(pub Arc); impl< T: PlaylistService, - > tonic::server::UnaryService - for ShufflePlaylistSvc { - type Response = super::ShufflePlaylistResponse; + > tonic::server::UnaryService + for MoveFolderSvc { + type Response = super::MoveFolderResponse; type Future = BoxFuture< tonic::Response, tonic::Status, >; fn call( &mut self, - request: tonic::Request, + request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::shuffle_playlist(&inner, request) - .await + ::move_folder(&inner, request).await }; Box::pin(fut) } @@ -6735,7 +7726,7 @@ pub mod playlist_service_server { let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); let fut = async move { - let method = ShufflePlaylistSvc(inner); + let method = MoveFolderSvc(inner); let codec = tonic::codec::ProstCodec::default(); let mut grpc = tonic::server::Grpc::new(codec) .apply_compression_config( diff --git a/crates/rpc/src/api/rockbox_descriptor.bin b/crates/rpc/src/api/rockbox_descriptor.bin index 5da9ae91af6..891e8f3fb09 100644 Binary files a/crates/rpc/src/api/rockbox_descriptor.bin and b/crates/rpc/src/api/rockbox_descriptor.bin differ diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs index 4f42d7a4071..209991f0b47 100644 --- a/crates/rpc/src/lib.rs +++ b/crates/rpc/src/lib.rs @@ -23,6 +23,7 @@ pub mod api { #[path = ""] pub mod rockbox { use rockbox_graphql::schema; + use rockbox_library::entity; use rockbox_sys::types::{ mp3_entry::Mp3Entry, system_status::SystemStatus, @@ -46,6 +47,39 @@ pub mod api { pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("api/rockbox_descriptor.bin"); + impl From for CurrentTrackResponse { + fn from(track: entity::track::Track) -> Self { + let id = track.id; + let title = track.title; + let artist = track.artist; + let album = track.album; + let genre = track.genre.unwrap_or_default(); + let path = track.path; + let album_art = track.album_art; + let album_id = track.album_id; + let artist_id = track.artist_id; + let length = track.length as u64; + let filesize = track.filesize as u64; + let bitrate = track.bitrate; + let frequency = track.frequency as u64; + + CurrentTrackResponse { + id, + title, + artist, + album, + genre, + path, + album_art, + length, + filesize, + bitrate, + frequency, + ..Default::default() + } + } + } + impl From for CurrentTrackResponse { fn from(mp3entry: Mp3Entry) -> Self { let title = mp3entry.title; diff --git a/crates/rpc/src/playback.rs b/crates/rpc/src/playback.rs index d02790ff892..25c92aa0af9 100644 --- a/crates/rpc/src/playback.rs +++ b/crates/rpc/src/playback.rs @@ -325,9 +325,28 @@ impl PlaybackService for Playback { async fn play_playlist( &self, - _request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { - todo!() + let request = request.into_inner(); + let playlist_id = request.playlist_id; + let shuffle = match request.shuffle { + Some(true) => 1, + Some(false) => 0, + None => 0, + }; + let url = format!( + "{}/playlists/{}/play?shuffle={}", + rockbox_url(), + playlist_id, + shuffle + ); + let client = reqwest::Client::new(); + client + .put(&url) + .send() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + Ok(tonic::Response::new(PlayPlaylistResponse::default())) } async fn play_directory( diff --git a/crates/rpc/src/playlist.rs b/crates/rpc/src/playlist.rs index a75d9d7e4b1..c89461fd33a 100644 --- a/crates/rpc/src/playlist.rs +++ b/crates/rpc/src/playlist.rs @@ -1,6 +1,6 @@ use std::sync::{mpsc::Sender, Arc, Mutex}; -use rockbox_library::repo; +use rockbox_library::{entity::folder::Folder, repo}; use rockbox_sys::{ events::RockboxCommand, types::{playlist_amount::PlaylistAmount, playlist_info::PlaylistInfo}, @@ -197,12 +197,14 @@ impl PlaylistService for Playlist { async fn remove_all_tracks( &self, - _request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { + let request = request.into_inner(); + let playlist_id = request.playlist_id.unwrap_or("current".to_string()); let body = serde_json::json!({ "positions": [], }); - let url = format!("{}/playlists/current/tracks", rockbox_url()); + let url = format!("{}/playlists/{}/tracks", rockbox_url(), playlist_id); self.client .delete(&url) .json(&body) @@ -217,10 +219,11 @@ impl PlaylistService for Playlist { request: tonic::Request, ) -> Result, tonic::Status> { let request = request.into_inner(); + let playlist_id = request.playlist_id.unwrap_or("current".to_string()); let body = serde_json::json!({ "positions": request.positions, }); - let url = format!("{}/playlists/current/tracks", rockbox_url()); + let url = format!("{}/playlists/{}/tracks", rockbox_url(), playlist_id); self.client .delete(&url) .json(&body) @@ -235,11 +238,17 @@ impl PlaylistService for Playlist { request: tonic::Request, ) -> Result, tonic::Status> { let request = request.into_inner(); - let body = serde_json::json!({ - "name": request.name, - "tracks": request.tracks, - "folder_id": request.folder_id, - }); + let body = match request.name { + Some(name) => serde_json::json!({ + "name": name, + "tracks": request.tracks, + "folder_id": request.folder_id, + }), + None => serde_json::json!({ + "tracks": request.tracks, + "folder_id": request.folder_id, + }), + }; let url = format!("{}/playlists", rockbox_url()); let response = self @@ -263,12 +272,14 @@ impl PlaylistService for Playlist { request: tonic::Request, ) -> Result, tonic::Status> { let request = request.into_inner(); + let playlist_id = request.playlist_id.unwrap_or("current".to_string()); let body = serde_json::json!({ "position": request.position, "tracks": request.tracks, }); - let url = format!("{}/playlists/current/tracks", rockbox_url()); - self.client + let url = format!("{}/playlists/{}/tracks", rockbox_url(), playlist_id); + let client = reqwest::Client::new(); + client .post(&url) .json(&body) .send() @@ -282,12 +293,13 @@ impl PlaylistService for Playlist { request: tonic::Request, ) -> Result, tonic::Status> { let request = request.into_inner(); + let playlist_id = request.playlist_id.unwrap_or("current".to_string()); let body = serde_json::json!({ "position": request.position, "tracks": [], "directory": request.directory, }); - let url = format!("{}/playlists/current/tracks", rockbox_url()); + let url = format!("{}/playlists/{}/tracks", rockbox_url(), playlist_id); self.client .post(&url) .json(&body) @@ -337,7 +349,9 @@ impl PlaylistService for Playlist { "position": position, "tracks": tracks, }); - let url = format!("{}/playlists/current/tracks", rockbox_url()); + let playlist_id = request.playlist_id.unwrap_or("current".to_string()); + let url = format!("{}/playlists/{}/tracks", rockbox_url(), playlist_id); + self.client .post(&url) .json(&body) @@ -363,7 +377,8 @@ impl PlaylistService for Playlist { "position": position, "tracks": tracks, }); - let url = format!("{}/playlists/current/tracks", rockbox_url()); + let playlist_id = request.playlist_id.unwrap_or("current".to_string()); + let url = format!("{}/playlists/{}/tracks", rockbox_url(), playlist_id); self.client .post(&url) .json(&body) @@ -373,4 +388,254 @@ impl PlaylistService for Playlist { Ok(tonic::Response::new(InsertArtistTracksResponse::default())) } + + async fn get_playlist( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + + if request.playlist_id.as_str() != "current" { + let tracks = + repo::playlist_tracks::find_by_playlist(self.pool.clone(), &request.playlist_id) + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + let playlist = repo::playlist::find(self.pool.clone(), &request.playlist_id) + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + + if let Some(playlist) = playlist { + return Ok(tonic::Response::new(GetPlaylistResponse { + id: playlist.id, + amount: tracks.len() as i32, + name: playlist.name, + folder_id: playlist.folder_id, + created_at: playlist.created_at.to_rfc3339(), + updated_at: playlist.updated_at.to_rfc3339(), + tracks: tracks + .into_iter() + .map(|track| CurrentTrackResponse::from(track)) + .collect(), + ..Default::default() + })); + } + } + + let url = format!("{}/playlists/{}", rockbox_url(), request.playlist_id); + let client = reqwest::Client::new(); + let response = client + .get(url) + .send() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + let playlist = response + .json::() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + let tracks = playlist + .entries + .iter() + .map(|track| CurrentTrackResponse::from(track.clone())) + .collect::>(); + Ok(tonic::Response::new(GetPlaylistResponse { + id: playlist.id.unwrap_or_default(), + amount: playlist.amount, + name: playlist.name.unwrap_or_default(), + folder_id: playlist.folder_id, + created_at: playlist.created_at.unwrap_or_default(), + updated_at: playlist.updated_at.unwrap_or_default(), + tracks, + ..Default::default() + })) + } + async fn get_playlists( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + let playlists = repo::playlist::find_by_folder(self.pool.clone(), request.folder_id) + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + let playlists = playlists + .into_iter() + .map(|playlist| GetPlaylistResponse { + id: playlist.id, + name: playlist.name, + folder_id: playlist.folder_id, + image: playlist.image, + description: playlist.description, + created_at: playlist.created_at.to_rfc3339(), + updated_at: playlist.updated_at.to_rfc3339(), + ..Default::default() + }) + .collect::>(); + Ok(tonic::Response::new(GetPlaylistsResponse { playlists })) + } + + async fn create_folder( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + let url = format!("{}/folders", rockbox_url()); + let body = match request.parent_id { + Some(parent_id) => serde_json::json!({ + "name": request.name, + "parent_id": parent_id, + }), + None => serde_json::json!({ + "name": request.name, + }), + }; + let client = reqwest::Client::new(); + let response = client + .post(&url) + .json(&body) + .send() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + let response = response + .json::() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + let folder_id = response.id; + + Ok(tonic::Response::new(CreateFolderResponse { folder_id })) + } + + async fn get_folder( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + let url = format!("{}/folders/{}", rockbox_url(), request.id); + let client = reqwest::Client::new(); + let response = client + .get(url) + .send() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + let folder = response + .json::() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + Ok(tonic::Response::new(GetFolderResponse { + id: folder.id, + name: folder.name, + parent_id: folder.parent_id, + })) + } + + async fn get_folders( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + let folders = repo::folder::find_by_parent(self.pool.clone(), request.parent_id) + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + let folders = folders + .into_iter() + .map(|folder| GetFolderResponse { + id: folder.id, + name: folder.name, + parent_id: folder.parent_id, + }) + .collect::>(); + Ok(tonic::Response::new(GetFoldersResponse { folders })) + } + + async fn remove_folder( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + let url = format!("{}/folders/{}", rockbox_url(), request.id); + let client = reqwest::Client::new(); + client + .delete(&url) + .send() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + Ok(tonic::Response::new(RemoveFolderResponse::default())) + } + + async fn remove_playlist( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + let url = format!("{}/playlists/{}", rockbox_url(), request.id); + let client = reqwest::Client::new(); + client + .delete(&url) + .send() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + Ok(tonic::Response::new(RemovePlaylistResponse::default())) + } + + async fn rename_playlist( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + let url = format!("{}/playlists/{}", rockbox_url(), request.id); + let client = reqwest::Client::new(); + client + .put(&url) + .json(&serde_json::json!({"name": request.name})) + .send() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + Ok(tonic::Response::new(RenamePlaylistResponse::default())) + } + + async fn rename_folder( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + let url = format!("{}/folders/{}", rockbox_url(), request.id); + let client = reqwest::Client::new(); + client + .put(&url) + .json(&serde_json::json!({"name": request.name})) + .send() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + Ok(tonic::Response::new(RenameFolderResponse::default())) + } + + async fn move_playlist( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + let url = format!("{}/playlists/{}", rockbox_url(), request.playlist_id); + let client = reqwest::Client::new(); + client + .put(&url) + .json(&serde_json::json!({"folder_id": request.folder_id})) + .send() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + Ok(tonic::Response::new(MovePlaylistResponse::default())) + } + + async fn move_folder( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + let url = format!("{}/folders/{}", rockbox_url(), request.folder_id); + let client = reqwest::Client::new(); + client + .put(&url) + .json(&serde_json::json!({"parent_id": request.parent_id})) + .send() + .await + .map_err(|e| tonic::Status::internal(e.to_string()))?; + Ok(tonic::Response::new(MoveFolderResponse::default())) + } } diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 71bda3e8593..60cd4865c18 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -9,6 +9,7 @@ crate-type = ["staticlib"] [dependencies] anyhow = "1.0.89" async-std = {version = "1.13.0", features = ["unstable"]} +cuid = "1.3.3" futures-util = "0.3.31" lazy_static = "1.5.0" local-ip-addr = "0.1.1" @@ -16,6 +17,7 @@ md5 = "0.7.0" owo-colors = "4.0.0" queryst = "3.0.0" rand = "0.8.5" +chrono = {version = "0.4.38", features = ["serde"]} reqwest = {version = "0.12.5", features = ["blocking", "rustls-tls"], default-features = false} rockbox-chromecast = {path = "../chromecast"} rockbox-discovery = {path = "../discovery"} diff --git a/crates/server/src/handlers/folders.rs b/crates/server/src/handlers/folders.rs new file mode 100644 index 00000000000..05246aa7dd7 --- /dev/null +++ b/crates/server/src/handlers/folders.rs @@ -0,0 +1,69 @@ +use crate::http::{Context, Request, Response}; +use anyhow::Error; +use cuid::cuid1; +use rockbox_library::{entity, repo}; +use rockbox_types::{Folder, FolderUpdate}; +use serde_json::json; + +pub async fn create_folder(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { + if req.body.is_none() { + res.set_status(400); + return Ok(()); + } + let body = req.body.as_ref().unwrap(); + let folder: Folder = serde_json::from_str(body)?; + let id = repo::folder::save( + ctx.pool.clone(), + entity::folder::Folder { + id: cuid1()?, + name: folder.name, + parent_id: folder.parent_id, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }, + ) + .await?; + res.json(&json!({ "id": id })); + Ok(()) +} + +pub async fn get_folder(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { + let folder = repo::folder::find(ctx.pool.clone(), &req.params[0]).await?; + res.json(&folder); + Ok(()) +} + +pub async fn get_folders(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { + let parent_id = req.query_params.get("parent_id"); + let parent_id = parent_id.map(|s| s.as_str().unwrap().to_string()); + let folders = repo::folder::find_by_parent(ctx.pool.clone(), parent_id).await?; + res.json(&folders); + Ok(()) +} + +pub async fn update_folder(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { + if req.body.is_none() { + res.set_status(400); + return Ok(()); + } + let body = req.body.as_ref().unwrap(); + let folder: FolderUpdate = serde_json::from_str(body)?; + repo::folder::update( + ctx.pool.clone(), + entity::folder::Folder { + id: req.params[0].clone(), + name: folder.name.unwrap_or_default(), + parent_id: folder.parent_id, + ..Default::default() + }, + ) + .await?; + res.json(&json!({ "id": req.params[0] })); + Ok(()) +} + +pub async fn delete_folder(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { + repo::folder::delete(ctx.pool.clone(), &req.params[0]).await?; + res.json(&json!({ "id": req.params[0] })); + Ok(()) +} diff --git a/crates/server/src/handlers/mod.rs b/crates/server/src/handlers/mod.rs index 6d4170d3a90..2891d4bd73e 100644 --- a/crates/server/src/handlers/mod.rs +++ b/crates/server/src/handlers/mod.rs @@ -3,6 +3,7 @@ pub mod artists; pub mod browse; pub mod devices; pub mod docs; +pub mod folders; pub mod player; pub mod playlists; pub mod search; @@ -60,6 +61,15 @@ async_handler!(playlists, get_playlist_tracks); async_handler!(playlists, insert_tracks); async_handler!(playlists, remove_tracks); async_handler!(playlists, get_playlist); +async_handler!(playlists, get_playlists); +async_handler!(playlists, delete_playlist); +async_handler!(playlists, update_playlist); +async_handler!(playlists, play_playlist); +async_handler!(folders, create_folder); +async_handler!(folders, get_folder); +async_handler!(folders, get_folders); +async_handler!(folders, update_folder); +async_handler!(folders, delete_folder); async_handler!(tracks, get_tracks); async_handler!(tracks, get_track); async_handler!(system, get_rockbox_version); diff --git a/crates/server/src/handlers/playlists.rs b/crates/server/src/handlers/playlists.rs index 59b5de71dc5..6e517d8f575 100644 --- a/crates/server/src/handlers/playlists.rs +++ b/crates/server/src/handlers/playlists.rs @@ -6,22 +6,23 @@ use anyhow::Error; use local_ip_addr::get_local_ip_address; use rand::seq::SliceRandom; use rockbox_graphql::read_files; -use rockbox_library::repo; +use rockbox_library::{entity, repo}; use rockbox_network::download_tracks; +use rockbox_sys::types::mp3_entry::Mp3Entry; use rockbox_sys::{ self as rb, types::{playlist_amount::PlaylistAmount, playlist_info::PlaylistInfo}, PLAYLIST_INSERT_LAST, PLAYLIST_INSERT_LAST_SHUFFLED, }; use rockbox_traits::types::track::Track; -use rockbox_types::{DeleteTracks, InsertTracks, NewPlaylist, StatusCode}; +use rockbox_types::{DeleteTracks, InsertTracks, NewPlaylist, PlaylistUpdate, StatusCode}; +use serde_json::json; pub async fn create_playlist( - _ctx: &Context, + ctx: &Context, req: &Request, res: &mut Response, ) -> Result<(), Error> { - let player_mutex = PLAYER_MUTEX.lock().unwrap(); if req.body.is_none() { res.set_status(400); return Ok(()); @@ -29,12 +30,57 @@ pub async fn create_playlist( let body = req.body.as_ref().unwrap(); let mut new_playlist: NewPlaylist = serde_json::from_str(body).unwrap(); + if let Some(playlist_name) = new_playlist.name.as_ref() { + if playlist_name.is_empty() { + res.set_status(400); + return Ok(()); + } + let playlist_id = repo::playlist::save( + ctx.pool.clone(), + entity::playlist::Playlist { + id: cuid::cuid1()?, + name: playlist_name.clone(), + folder_id: new_playlist.folder_id, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + ..Default::default() + }, + ) + .await?; + + let tracks = download_tracks(new_playlist.tracks).await?; + for (position, track_path) in tracks.iter().enumerate() { + let hash = format!("{:x}", md5::compute(track_path.as_bytes())); + let track = repo::track::find_by_md5(ctx.pool.clone(), &hash).await?; + if track.is_none() { + continue; + } + repo::playlist_tracks::save( + ctx.pool.clone(), + entity::playlist_tracks::PlaylistTracks { + id: cuid::cuid1()?, + playlist_id: playlist_id.clone(), + track_id: track.unwrap().id, + position: position as u32, + created_at: chrono::Utc::now(), + }, + ) + .await?; + } + + res.set_status(200); + res.text("0"); + return Ok(()); + } + if new_playlist.tracks.is_empty() { return Ok(()); } new_playlist.tracks = download_tracks(new_playlist.tracks).await?; + let player_mutex = PLAYER_MUTEX.lock().unwrap(); + let dir = new_playlist.tracks[0].clone(); let dir_parts: Vec<_> = dir.split('/').collect(); let dir = dir_parts[0..dir_parts.len() - 1].join("/"); @@ -143,10 +189,37 @@ pub async fn resume_track( } pub async fn get_playlist_tracks( - _ctx: &Context, - _req: &Request, + ctx: &Context, + req: &Request, res: &mut Response, ) -> Result<(), Error> { + let playlist_id = &req.params[0]; + + if playlist_id != "current" { + let tracks = repo::playlist_tracks::find_by_playlist(ctx.pool.clone(), playlist_id).await?; + let mut entries: Vec = vec![]; + + for track in tracks { + entries.push(Mp3Entry { + id: Some(track.id), + path: track.path, + title: track.title, + artist: track.artist, + album: track.album, + length: track.length as u64, + tracknum: track.track_number.unwrap_or_default() as i32, + album_art: track.album_art, + artist_id: Some(track.artist_id), + album_id: Some(track.album_id), + filesize: track.filesize as u64, + frequency: track.frequency as u64, + ..Default::default() + }); + } + res.json(&entries); + return Ok(()); + } + let player_mutex = PLAYER_MUTEX.lock().unwrap(); let mut entries = vec![]; let amount = rb::playlist::amount(); @@ -164,6 +237,45 @@ pub async fn get_playlist_tracks( } pub async fn insert_tracks(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { + let playlist_id = &req.params[0]; + if playlist_id != "current" { + let req_body = req.body.as_ref().unwrap(); + let mut tracklist: InsertTracks = serde_json::from_str(&req_body)?; + let tracks = download_tracks(tracklist.tracks).await?; + + if let Some(dir) = &tracklist.directory { + tracklist.tracks = read_files(dir.clone()).await?; + } + + let playlist = repo::playlist::find(ctx.pool.clone(), playlist_id).await?; + if playlist.is_none() { + res.set_status(404); + return Ok(()); + } + + for track_path in tracks { + let current_tracks = + repo::playlist_tracks::find_by_playlist(ctx.pool.clone(), playlist_id).await?; + let hash = format!("{:x}", md5::compute(track_path.as_bytes())); + + if let Some(track) = repo::track::find_by_md5(ctx.pool.clone(), &hash).await? { + repo::playlist_tracks::save( + ctx.pool.clone(), + entity::playlist_tracks::PlaylistTracks { + id: cuid::cuid1()?, + playlist_id: playlist_id.clone(), + track_id: track.id, + position: current_tracks.len() as u32, + created_at: chrono::Utc::now(), + }, + ) + .await?; + } + } + res.text("0"); + return Ok(()); + } + let player_mutex = PLAYER_MUTEX.lock().unwrap(); let req_body = req.body.as_ref().unwrap(); let mut tracklist: InsertTracks = serde_json::from_str(&req_body).unwrap(); @@ -262,6 +374,24 @@ pub async fn insert_tracks(ctx: &Context, req: &Request, res: &mut Response) -> } pub async fn remove_tracks(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { + let playlist_id = &req.params[0]; + if playlist_id != "current" { + let req_body = req.body.as_ref().unwrap(); + let params = serde_json::from_str::(&req_body)?; + + if params.positions.is_empty() { + res.text("0"); + return Ok(()); + } + + for position in params.positions { + repo::playlist_tracks::delete_track_at(ctx.pool.clone(), playlist_id, position as u32) + .await?; + } + res.text("0"); + return Ok(()); + } + let player_mutex = PLAYER_MUTEX.lock().unwrap(); let player = ctx.player.lock().unwrap(); @@ -332,7 +462,45 @@ pub async fn current_playlist( Ok(()) } -pub async fn get_playlist(ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> { +pub async fn get_playlist(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { + let playlist_id = &req.params[0]; + if playlist_id != "current" { + let playlist = repo::playlist::find(ctx.pool.clone(), playlist_id).await?; + if playlist.is_none() { + res.set_status(404); + return Ok(()); + } + + let playlist = playlist.unwrap(); + + let tracks = repo::playlist_tracks::find_by_playlist(ctx.pool.clone(), playlist_id).await?; + let mut entries: Vec = vec![]; + for track in tracks.clone() { + let mut entry = rb::metadata::get_metadata(-1, &track.path); + entry.album_art = track.album_art; + entry.album_id = Some(track.album_id); + entry.artist_id = Some(track.artist_id); + entry.genre_id = Some(track.genre_id); + entry.id = Some(track.id); + entries.push(entry); + } + + let result = PlaylistInfo { + id: Some(playlist.id), + amount: tracks.len() as i32, + entries, + name: Some(playlist.name), + folder_id: playlist.folder_id, + image: playlist.image, + description: playlist.description, + created_at: Some(playlist.created_at.to_rfc3339()), + updated_at: Some(playlist.updated_at.to_rfc3339()), + ..Default::default() + }; + res.json(&result); + return Ok(()); + } + let player_mutex = PLAYER_MUTEX.lock().unwrap(); let mut player = ctx.player.lock().unwrap(); @@ -399,3 +567,106 @@ pub async fn get_playlist(ctx: &Context, _req: &Request, res: &mut Response) -> drop(player_mutex); Ok(()) } + +pub async fn delete_playlist( + ctx: &Context, + req: &Request, + res: &mut Response, +) -> Result<(), Error> { + let playlist_id = &req.params[0]; + repo::playlist::delete(ctx.pool.clone(), playlist_id).await?; + repo::playlist_tracks::delete_by_playlist(ctx.pool.clone(), playlist_id).await?; + res.json(&json!({ "id": playlist_id })); + Ok(()) +} + +pub async fn update_playlist( + ctx: &Context, + req: &Request, + res: &mut Response, +) -> Result<(), Error> { + let playlist_id = &req.params[0]; + if req.body.is_none() { + res.set_status(400); + return Ok(()); + } + + let body = req.body.as_ref().unwrap(); + let playlist: PlaylistUpdate = serde_json::from_str(body)?; + repo::playlist::update( + ctx.pool.clone(), + entity::playlist::Playlist { + id: playlist_id.clone(), + name: playlist.name.unwrap_or_default(), + folder_id: playlist.folder_id, + ..Default::default() + }, + ) + .await?; + + res.json(&json!({ "id": playlist_id })); + Ok(()) +} + +pub async fn get_playlists(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { + let folder_id = req.query_params.get("folder_id"); + let folder_id = folder_id.map(|f| f.as_str().unwrap().to_string()); + let playlist = repo::playlist::find_by_folder(ctx.pool.clone(), folder_id).await?; + res.json(&playlist); + Ok(()) +} + +pub async fn play_playlist(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { + let playlist_id = &req.params[0]; + let tracks = repo::playlist_tracks::find_by_playlist(ctx.pool.clone(), playlist_id).await?; + + if tracks.is_empty() { + res.set_status(200); + return Ok(()); + } + + let player_mutex = PLAYER_MUTEX.lock().unwrap(); + + let dir = tracks[0].clone(); + let dir = dir.path.clone(); + let dir_parts: Vec<_> = dir.split('/').collect(); + let dir = dir_parts[0..dir_parts.len() - 1].join("/"); + let status = rb::playlist::create(&dir, None); + if status == -1 { + res.set_status(500); + return Ok(()); + } + rb::playlist::build_playlist( + tracks.iter().map(|t| t.path.as_str()).collect(), + 0, + tracks.len() as i32, + ); + + let shuffle = match req.query_params.get("shuffle") { + Some(shuffle) => shuffle.as_str().unwrap_or("0").parse().unwrap_or(0), + None => 0, + }; + + if shuffle == 1 { + let seed = rb::system::current_tick(); + rb::playlist::shuffle(seed as i32, 0); + } + + let start_index = match req.query_params.get("start_index") { + Some(start_index) => start_index.as_str().unwrap_or("0").parse().unwrap_or(0), + None => 0, + }; + let elapsed = match req.query_params.get("elapsed") { + Some(elapsed) => elapsed.as_str().unwrap_or("0").parse().unwrap_or(0), + None => 0, + }; + let offset = match req.query_params.get("offset") { + Some(offset) => offset.as_str().unwrap_or("0").parse().unwrap_or(0), + None => 0, + }; + rb::playlist::start(start_index, elapsed, offset); + + res.set_status(200); + drop(player_mutex); + Ok(()) +} diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index e86ef894e9b..3f789d5296c 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -82,6 +82,7 @@ pub extern "C" fn start_server() { app.put("/player/volume", adjust_volume); app.post("/playlists", create_playlist); + app.get("/playlists", get_playlists); app.put("/playlists/start", start_playlist); app.put("/playlists/shuffle", shuffle_playlist); app.get("/playlists/amount", get_playlist_amount); @@ -91,6 +92,15 @@ pub extern "C" fn start_server() { app.post("/playlists/:id/tracks", insert_tracks); app.delete("/playlists/:id/tracks", remove_tracks); app.get("/playlists/:id", get_playlist); + app.put("/playlists/:id", update_playlist); + app.delete("/playlists/:id", delete_playlist); + app.put("/playlists/:id/play", play_playlist); + + app.post("/folders", create_folder); + app.get("/folders", get_folders); + app.get("/folders/:id", get_folder); + app.put("/folders/:id", update_folder); + app.delete("/folders/:id", delete_folder); app.get("/tracks", get_tracks); app.get("/tracks/:id", get_track); @@ -346,6 +356,7 @@ pub extern "C" fn start_broker() { .into_iter() .map(|t| t.into()) .collect(), + ..Default::default() }); thread::sleep(std::time::Duration::from_millis(100)); diff --git a/crates/sys/src/types/playlist_info.rs b/crates/sys/src/types/playlist_info.rs index 8f867a326e0..e4aa222f76b 100644 --- a/crates/sys/src/types/playlist_info.rs +++ b/crates/sys/src/types/playlist_info.rs @@ -24,6 +24,13 @@ pub struct PlaylistInfo { pub control_filename: String, // char control_filename[sizeof(PLAYLIST_CONTROL_FILE) + 8] pub dcfrefs_handle: i32, // int dcfrefs_handle pub entries: Vec, + pub name: Option, + pub created_at: Option, + pub updated_at: Option, + pub folder_id: Option, + pub image: Option, + pub description: Option, + pub id: Option, } impl From for PlaylistInfo { @@ -56,6 +63,7 @@ impl From for PlaylistInfo { }, dcfrefs_handle: info.dcfrefs_handle, entries: vec![], + ..Default::default() } } } diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 264d1b7c3d2..f9b1fbb279f 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -11,6 +11,7 @@ pub mod device; pub struct NewPlaylist { pub name: Option, pub tracks: Vec, + pub folder_id: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -68,3 +69,32 @@ pub struct ReplaygainSettings { pub peak: i32, pub clip: i32, } + +#[derive(Debug, Serialize, Deserialize)] +pub struct Folder { + pub name: String, + pub parent_id: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FolderUpdate { + pub name: Option, + pub parent_id: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PlaylistUpdate { + pub name: Option, + pub folder_id: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Playlist { + pub id: String, + pub name: String, + pub created_at: u64, + pub updated_at: u64, + pub folder_id: Option, + pub image: Option, + pub description: Option, +} diff --git a/deno b/deno index 287e34d4cb1..92ad8de5092 160000 --- a/deno +++ b/deno @@ -1 +1 @@ -Subproject commit 287e34d4cb14989ec37c98cd30fd60e04bb683a8 +Subproject commit 92ad8de509247df9887e228469fac87ab38e1ef0 diff --git a/gtk/blueprint-compiler/.coveragerc b/gtk/blueprint-compiler/.coveragerc new file mode 100644 index 00000000000..86fa04f6b59 --- /dev/null +++ b/gtk/blueprint-compiler/.coveragerc @@ -0,0 +1,6 @@ +[report] +exclude_lines = + pragma: no cover + raise AssertionError + raise NotImplementedError + raise CompilerBugError diff --git a/gtk/blueprint-compiler/.gitignore b/gtk/blueprint-compiler/.gitignore new file mode 100644 index 00000000000..9aa4dd4b6ec --- /dev/null +++ b/gtk/blueprint-compiler/.gitignore @@ -0,0 +1,16 @@ +__pycache__ +/build +/dist +*.egg-info +blueprint-compiler.pc + +/.coverage +/htmlcov +coverage.xml +.mypy_cache +/blueprint-regression-tests + +/corpus +/crashes + +.vscode \ No newline at end of file diff --git a/gtk/blueprint-compiler/.gitlab-ci.yml b/gtk/blueprint-compiler/.gitlab-ci.yml new file mode 100644 index 00000000000..1ec071e4ce3 --- /dev/null +++ b/gtk/blueprint-compiler/.gitlab-ci.yml @@ -0,0 +1,59 @@ +stages: +- build +- pages + +build: + image: registry.gitlab.gnome.org/jwestman/blueprint-compiler + stage: build + script: + - black --check --diff ./ tests + - isort --check --diff --profile black ./ tests + - mypy --python-version=3.9 blueprintcompiler/ + - G_DEBUG=fatal-warnings xvfb-run coverage run -m unittest + - coverage report + - coverage html + - coverage xml + - meson _build -Ddocs=true --prefix=/usr + - ninja -C _build + - ninja -C _build install + - ninja -C _build docs/en + - git clone https://gitlab.gnome.org/jwestman/blueprint-regression-tests.git + - cd blueprint-regression-tests + - git checkout 5f9e155c1333e84e6f683cdb26b02a5925fd8db3 + - ./test.sh + - cd .. + coverage: '/TOTAL.*\s([.\d]+)%/' + artifacts: + paths: + - _build + - htmlcov + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml + +fuzz: + image: registry.gitlab.gnome.org/jwestman/blueprint-compiler + stage: build + script: + - meson _build + - ninja -C _build install + - ./tests/fuzz.sh 5000 + artifacts: + when: always + paths: + - corpus + - crashes + +pages: + stage: pages + dependencies: + - build + script: + - mv _build/docs/en public + - mv htmlcov public/coverage + artifacts: + paths: + - public + only: + - main diff --git a/gtk/blueprint-compiler/CONTRIBUTING.md b/gtk/blueprint-compiler/CONTRIBUTING.md new file mode 100644 index 00000000000..20a865dd29b --- /dev/null +++ b/gtk/blueprint-compiler/CONTRIBUTING.md @@ -0,0 +1,28 @@ +First of all, thank you for contributing to Blueprint. + +If you learn something useful, please add it to this file. + +# Run the test suite + +```sh +python -m unittest +``` + +# Formatting + +Blueprint uses [Black](https://github.com/psf/black) for code formatting. + +# Build the docs + +```sh +pip install -U sphinx furo + +meson -Ddocs=true build +# or +meson --reconfigure -Ddocs=true build + +ninja -C build docs/en + +python -m http.server 2310 --bind 127.0.0.1 --directory build/docs/en/ +xdg-open http://127.0.0.1:2310/ +``` diff --git a/gtk/blueprint-compiler/COPYING b/gtk/blueprint-compiler/COPYING new file mode 100644 index 00000000000..0a041280bd0 --- /dev/null +++ b/gtk/blueprint-compiler/COPYING @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/gtk/blueprint-compiler/MAINTENANCE.md b/gtk/blueprint-compiler/MAINTENANCE.md new file mode 100644 index 00000000000..220c1171388 --- /dev/null +++ b/gtk/blueprint-compiler/MAINTENANCE.md @@ -0,0 +1,18 @@ +## Releasing a new version + +1. Look at the git log since the previous release. Note every significant change +in the NEWS file. +2. Update the version number, according to semver: + - At the top of meson.build + - In docs/flatpak.rst +3. Make a new commit with just these two changes. Use `Release v{version}` as the commit message. Tag the commit as `v{version}` and push the tag. +4. Create a "Post-release version bump" commit. +5. Go to the Releases page in GitLab and create a new release from the tag. +6. Announce the release through relevant channels (Twitter, TWIG, etc.) + +## Related projects + +Blueprint is supported by the following syntax highlighters. If changes are made to the syntax, remember to update these projects as well. + +- Pygments (https://github.com/pygments/pygments/blob/master/pygments/lexers/blueprint.py) +- GtkSourceView (https://gitlab.gnome.org/GNOME/gtksourceview/-/blob/master/data/language-specs/blueprint.lang) \ No newline at end of file diff --git a/gtk/blueprint-compiler/NEWS.md b/gtk/blueprint-compiler/NEWS.md new file mode 100644 index 00000000000..389f82c612c --- /dev/null +++ b/gtk/blueprint-compiler/NEWS.md @@ -0,0 +1,212 @@ +# v0.14.0 + +## Added +- Added a warning for unused imports. +- Added an option to not print the diff when formatting with the CLI. (Gregor Niehl) +- Added support for building Gtk.ColumnViewRow, Gtk.ColumnViewCell, and Gtk.ListHeader widgets with Gtk.BuilderListItemFactory. +- Added support for the `after` keyword for signals. This was previously documented but not implemented. (Gregor Niehl) +- Added support for string arrays. (Diego Augusto) +- Added hover documentation for properties in lookup expressions. +- The decompiler supports action widgets, translation domains, `typeof<>` syntax, and expressions. It also supports extension syntax for Adw.Breakpoint, Gtk.BuilderListItemFactory, Gtk.ComboBoxText, Gtk.SizeGroup, and Gtk.StringList. +- Added a `decompile` subcommand to the CLI, which decompiles an XML .ui file to blueprint. +- Accessibility relations that allow multiple values are supported using list syntax. (Julian Schmidhuber) + +## Changed +- The decompiler sorts imports alphabetically. +- Translatable strings use `translatable="yes"` instead of `translatable="true"` for compatibility with xgettext. (Marco Köpcke) +- The first line of the documentation is shown in the completion list when using the language server. (Sonny Piers) +- Object autocomplete uses a snippet to add the braces and position the cursor inside them. (Sonny Piers) +- The carets in the CLI diagnostic output now span the whole error message up to the end of the first line, rather than just the first character. +- The decompiler emits double quotes, which are compatible with gettext. + +## Fixed +- Fixed deprecation warnings in the language server. +- The decompiler no longer duplicates translator comments on properties. +- Subtemplates no longer output a redundant `@generated` comment. +- When extension syntax from a library that is not available is used, the compiler emits an error instead of crashing. +- The language server reports semantic token positions correctly. (Szepesi Tibor) +- The decompiler no longer emits the deprecated `bind-property` syntax. (Sonny Piers) +- Fixed the tests when used as a Meson subproject. (Benoit Pierre) +- Signal autocomplete generates correct syntax. (Sonny Piers) +- The decompiler supports templates that do not specify a parent class. (Sonny Piers) +- Adw.Breakpoint setters that set a property on the template no longer cause a crash. +- Fixed type checking with templates that do not have a parent class. +- Fixed online documentation links for interfaces. +- The wording of edit suggestions is fixed for insertions and deletions. +- When an input file uses tabs instead of spaces, the diagnostic output on the CLI aligns the caret correctly. +- The decompiler emits correct syntax when a property binding refers to the template object. + +## Documentation +- Fixed typos in "Built with Blueprint" section. (Valéry Febvre, Dexter Reed) + +# v0.12.0 + +## Added + +- Add support for Adw.AlertDialog (Sonny Piers) +- Emit warnings for deprecated APIs - lsp and compiler +- lsp: Document symbols +- lsp: "Go to definition" (ctrl+click) +- lsp: Code action for "namespace not imported" diagnostics, that adds the missing import +- Add a formatter - cli and lsp (Gregor Niehl) +- Support for translation domain - see documentation +- cli: Print code actions in error messages + +## Changed + +- compiler: Add a header notice mentionning the file is generated (Urtsi Santsi) +- decompiler: Use single quotes for output + +## Fixed + +- Fixed multine strings support with the escape newline character +- lsp: Fixed the signal completion, which was missing the "$" +- lsp: Fixed property value completion (Ivan Kalinin) +- lsp: Added a missing semantic highlight (for the enum in Gtk.Scale marks) +- Handle big endian bitfields correctly (Jerry James) +- batch-compile: Fix mixing relative and absolute paths (Marco Köpcke ) + +## Documentation + +- Fix grammar for bindings +- Add section on referencing templates + +# v0.10.0 + +## Added + +- The hover documentation now includes a link to the online documentation for the symbol, if available. +- Added hover documentation for the Adw.Breakpoint extensions, `condition` and `setters`. + +## Changed + +- Decompiling an empty file now produces an empty file rather than an error. (AkshayWarrier) +- More relevant documentation is shown when hovering over an identifier literal (such as an enum value or an object ID). + +## Fixed + +- Fixed an issue with the language server not conforming the spec. (seshotake) +- Fixed the signature section of the hover documentation for properties and signals. +- Fixed a bug where documentation was sometimes shown for a different symbol with the same name. +- Fixed a bug where documentation was not shown for accessibility properties that contain `-`. +- Number literals are now correctly parsed as floats if they contain a `.`, even if they are divisible by 1. + +## Removed + +- The `bind-property` keyword has been removed. Use `bind` instead. The old syntax is still accepted with a warning. + +## Documentation + +- Fixed the grammar for Extension, which was missing ExtAdwBreakpoint. + + +# v0.8.1 + +## Breaking Changes + +- Duplicates in a number of places are now considered errors. For example, duplicate flags in several places, duplicate + strings in Gtk.FileFilters, etc. + +## Fixed + +- Fixed a number of bugs in the XML output when using `template` to refer to the template object. + +## Documentation + +- Fixed the example for ExtListItemFactory + +# v0.8.0 + +## Breaking Changes + +- A trailing `|` is no longer allowed in flags. +- The primitive type names `gboolean`, `gchararray`, `gint`, `gint64`, `guint`, `guint64`, `gfloat`, `gdouble`, `utf8`, and `gtype` are no longer permitted. Use the non-`g`-prefixed versions instead. +- Translated strings may no longer have trailing commas. + +## Added + +- Added cast expressions, which are sometimes needed to specify type information in expressions. +- Added support for closure expressions. +- Added the `--typelib-path` command line argument, which allows adding directories to the search path for typelib files. +- Added custom compile and decompile commands to the language server. (Sonny Piers) +- Added support for [Adw.MessageDialog](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.MessageDialog.html#adwmessagedialog-as-gtkbuildable) custom syntax. +- Added support for inline sub-templates for [Gtk.BuilderListItemFactory](https://docs.gtk.org/gtk4/class.BuilderListItemFactory.html). (Cameron Dehning) +- Added support for [Adw.Breakpoint](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.Breakpoint.html) custom syntax. +- Added a warning when an object ID might be confusing. +- Added support for [Gtk.Scale](https://docs.gtk.org/gtk4/class.Scale.html#gtkscale-as-gtkbuildable) custom syntax. + +## Changed + +Some of these changes affect syntax, but the old syntax is still accepted with a purple "upgrade" warning, so they are not breaking changes yet. In editors that support code actions, such as Visual Studio Code, the blueprint language server can automatically fix these warnings. + +- The XML output uses the integer value rather than GIR name for enum values. +- Compiler errors are now printed to stderr rather than stdout. (Sonny Piers) +- Introduced `$` to indicate types or callbacks that are provided in application code. + - Types that are provided by application code are now begin with a `$` rather than a leading `.`. + - The handler name in a signal is now prefixed with `$`. + - Closure expressions, which were added in this version, are also prefixed with `$`. +- When a namespace is not found, errors are supressed when the namespace is used. +- The compiler bug message now reports the version of blueprint-compiler. +- The `typeof` syntax now uses `<>` instead of `()` to match cast expressions. +- Menu sections and subsections can now have an ID. +- The interactive porting tool now ignores hidden folders. (Sonny Piers) +- Templates now use the typename syntax rather than an ID to specify the template's class. In most cases, this just means adding a `$` prefix to the ID, but for GtkListItem templates it should be shortened to ListItem (since the Gtk namespace is implied). The template object is now referenced with the `template` keyword rather than with the ID. + +## Fixed + +- Fixed a bug in the language server's acceptance of text change commands. (Sonny Piers) +- Fixed a bug in the display of diagnostics when the diagnostic is at the beginning of a line. +- Fixed a crash that occurred when dealing with array types. +- Fixed a bug that prevented Gio.File properties from being settable. + +## Documentation + +- Added a reference section to the documentation. This replaces the Examples page with a detailed description of each syntax feature, including a formal specification of the grammar. + +# v0.6.0 + +## Breaking Changes +- Quoted and numeric literals are no longer interchangeable (e.g. `"800"` is no longer an accepted value for an + integer type). +- Boxed types are now type checked. + +## Added +- There is now syntax for `GType` literals: the `typeof()` pseudo-function. For example, list stores have an `item-type` + property which is now specifiable like this: `item-type: typeof(.MyDataModel)`. See the documentation for more details. + +## Changed +- The language server now logs to stderr. + +## Fixed +- Fix the build on Windows, where backslashes in paths were not escaped. (William Roy) +- Remove the syntax for specifying menu objects inline, since it does not work. +- Fix a crash in the language server that was triggered in files with incomplete `using Gtk 4.0;` statements. +- Fixed compilation on big-endian systems. +- Fix an issue in the interactive port tool that would lead to missed files. (Frank Dana) + +## Documentation +- Fix an issue for documentation contributors where changing the documentation files would not trigger a rebuild. +- Document the missing support for Gtk.Label ``, which is intentional, and recommend alternatives. (Sonny + Piers) +- Add a prominent warning that Blueprint is still experimental + + +# v0.4.0 + +## Added +- Lookup expressions +- With the language server, hovering over a diagnostic message now shows any + associated hints. + +## Changed +- The compiler now uses .typelib files rather than XML .gir files, which reduces + dependencies and should reduce compile times by about half a second. + +## Fixed +- Fix the decompiler/porting tool not importing the Adw namespace when needed +- Fix a crash when trying to compile an empty file +- Fix parsing of number tokens +- Fix a bug where action widgets did not work in templates +- Fix a crash in the language server that occurred when a `using` statement had +no version +- If a compiler bug is reported, the process now exits with a non-zero code diff --git a/gtk/blueprint-compiler/README.md b/gtk/blueprint-compiler/README.md new file mode 100644 index 00000000000..503d7a3e144 --- /dev/null +++ b/gtk/blueprint-compiler/README.md @@ -0,0 +1,108 @@ +# Blueprint + +A markup language for GTK user interface files. + +## Motivation + +GtkBuilder XML format is quite verbose, and many app developers don't like +using WYSIWYG editors for creating UIs. Blueprint files are intended to be a +concise, easy-to-read format that makes it easier to create and edit GTK UIs. + +Internally, it compiles to GtkBuilder XML as part of an app's build system. It +adds no new features, just makes the features that exist more accessible. + +Another goal is to have excellent developer tooling--including a language +server--so that less knowledge of the format is required. Hopefully this will +increase adoption of cool advanced features like GtkExpression. + +## Example + +Here is what [the libshumate demo's UI definition](https://gitlab.gnome.org/GNOME/libshumate/-/blob/main/demos/shumate-demo-window.ui) +looks like ported to this new format: + +``` +using Gtk 4.0; +using Shumate 1.0; + +template ShumateDemoWindow : Gtk.ApplicationWindow { + can-focus: yes; + title: _("Shumate Demo"); + default-width: 800; + default-height: 600; + + [titlebar] + Gtk.HeaderBar { + Gtk.DropDown layers_dropdown { + notify::selected => on_layers_dropdown_notify_selected() swapped; + } + } + + Gtk.Overlay overlay { + vexpand: true; + Shumate.Map map {} + + [overlay] + Shumate.Scale scale { + halign: start; + valign: end; + } + + [overlay] + Gtk.Box { + orientation: vertical; + halign: end; + valign: end; + + Shumate.Compass compass { + halign: end; + map: map; + } + Shumate.License license { + halign: end; + } + } + } +} +``` + +## Editors + +[Workbench](https://github.com/sonnyp/Workbench) and [GNOME Builder](https://apps.gnome.org/app/org.gnome.Builder/) have builtin support for Blueprint. + +Vim + +- [Syntax highlighting by thetek42](https://github.com/thetek42/vim-blueprint-syntax) +- [Syntax highlighting by gabmus](https://gitlab.com/gabmus/vim-blueprint) + +GNU Emacs + +- [Major mode by DrBluefall](https://github.com/DrBluefall/blueprint-mode) + +Visual Studio Code + +- [Blueprint Language Plugin by bodil](https://github.com/bodil/vscode-blueprint) + +## Donate + +You can support my work on GitHub Sponsors! + +## Getting in Touch + +Matrix room: [#blueprint-language:matrix.org](https://matrix.to/#/#blueprint-language:matrix.org) + +## License + +Copyright (C) 2021 James Westman + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . diff --git a/gtk/blueprint-compiler/blueprint-compiler.pc.in b/gtk/blueprint-compiler/blueprint-compiler.pc.in new file mode 100644 index 00000000000..c000a8a60e9 --- /dev/null +++ b/gtk/blueprint-compiler/blueprint-compiler.pc.in @@ -0,0 +1,3 @@ +Name: blueprint-compiler +Description: Markup compiler for GTK user interface definitions +Version: @VERSION@ diff --git a/gtk/blueprint-compiler/blueprint-compiler.py b/gtk/blueprint-compiler/blueprint-compiler.py new file mode 100755 index 00000000000..0c5c3fda0fd --- /dev/null +++ b/gtk/blueprint-compiler/blueprint-compiler.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +# blueprint-compiler.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import os +import sys + +# These variables should be set by meson. If they aren't, we're running +# uninstalled, and we might have to guess some values. +version = "@VERSION@" +module_path = r"@MODULE_PATH@" +libdir = r"@LIBDIR@" + +if version == "\u0040VERSION@": + version = "uninstalled" + libdir = None +else: + # If Meson set the configuration values, insert the module path it set + sys.path.insert(0, module_path) + +from blueprintcompiler import main + +if __name__ == "__main__": + main.main(version, libdir) diff --git a/gtk/blueprint-compiler/blueprintcompiler/__init__.py b/gtk/blueprint-compiler/blueprintcompiler/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gtk/blueprint-compiler/blueprintcompiler/ast_utils.py b/gtk/blueprint-compiler/blueprintcompiler/ast_utils.py new file mode 100644 index 00000000000..bd5befa8c29 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/ast_utils.py @@ -0,0 +1,320 @@ +# ast_utils.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import typing as T +from collections import ChainMap, defaultdict +from functools import cached_property + +from .errors import * +from .lsp_utils import DocumentSymbol, LocationLink, SemanticToken +from .tokenizer import Range + +TType = T.TypeVar("TType") + + +class Children: + """Allows accessing children by type using array syntax.""" + + def __init__(self, children): + self._children = children + + def __iter__(self) -> T.Iterator["AstNode"]: + return iter(self._children) + + @T.overload + def __getitem__(self, key: T.Type[TType]) -> T.List[TType]: ... + + @T.overload + def __getitem__(self, key: int) -> "AstNode": ... + + def __getitem__(self, key): + if isinstance(key, int): + if key >= len(self._children): + return None + else: + return self._children[key] + else: + return [child for child in self._children if isinstance(child, key)] + + +class Ranges: + def __init__(self, ranges: T.Dict[str, Range]): + self._ranges = ranges + + def __getitem__(self, key: T.Union[str, tuple[str, str]]) -> T.Optional[Range]: + if isinstance(key, str): + return self._ranges.get(key) + elif isinstance(key, tuple): + start, end = key + return Range.join(self._ranges.get(start), self._ranges.get(end)) + + +TCtx = T.TypeVar("TCtx") +TAttr = T.TypeVar("TAttr") + + +class Ctx: + """Allows accessing values from higher in the syntax tree.""" + + def __init__(self, node: "AstNode") -> None: + self.node = node + + def __getitem__(self, key: T.Type[TCtx]) -> T.Optional[TCtx]: + attrs = self.node._attrs_by_type(Context) + for name, attr in attrs: + if attr.type == key: + return getattr(self.node, name) + if self.node.parent is not None: + return self.node.parent.context[key] + else: + return None + + +class AstNode: + """Base class for nodes in the abstract syntax tree.""" + + completers: T.List = [] + attrs_by_type: T.Dict[T.Type, T.List] = {} + + def __init__(self, group, children, tokens, incomplete=False): + self.group = group + self.children = Children(children) + self.tokens = ChainMap(tokens, defaultdict(lambda: None)) + self.incomplete = incomplete + + self.parent = None + for child in self.children: + child.parent = self + + def __init_subclass__(cls): + cls.completers = [] + cls.validators = [ + getattr(cls, f) for f in dir(cls) if hasattr(getattr(cls, f), "_validator") + ] + cls.attrs_by_type = {} + + @cached_property + def context(self): + return Ctx(self) + + @cached_property + def ranges(self): + return Ranges(self.group.ranges) + + @cached_property + def root(self): + if self.parent is None: + return self + else: + return self.parent.root + + @property + def range(self) -> Range: + return Range(self.group.start, self.group.end, self.group.text) + + def parent_by_type(self, type: T.Type[TType]) -> TType: + if self.parent is None: + raise CompilerBugError() + elif isinstance(self.parent, type): + return self.parent + else: + return self.parent.parent_by_type(type) + + @cached_property + def errors(self): + return list( + error + for error in self._get_errors() + if not isinstance(error, CompileWarning) + ) + + @cached_property + def warnings(self): + return list( + warning + for warning in self._get_errors() + if isinstance(warning, CompileWarning) + ) + + def _get_errors(self): + for validator in self.validators: + try: + validator(self) + except CompileError as e: + yield e + if e.fatal: + return + + for child in self.children: + yield from child._get_errors() + + def _attrs_by_type(self, attr_type: T.Type[TAttr]) -> T.List[T.Tuple[str, TAttr]]: + if attr_type not in self.attrs_by_type: + self.attrs_by_type[attr_type] = [] + for name in dir(type(self)): + item = getattr(type(self), name) + if isinstance(item, attr_type): + self.attrs_by_type[attr_type].append((name, item)) + return self.attrs_by_type[attr_type] + + def get_docs(self, idx: int) -> T.Optional[str]: + for name, attr in self._attrs_by_type(Docs): + if attr.token_name: + token = self.group.tokens.get(attr.token_name) + if token and token.start <= idx < token.end: + return getattr(self, name) + + for child in self.children: + if idx in child.range: + if docs := child.get_docs(idx): + return docs + + for name, attr in self._attrs_by_type(Docs): + if not attr.token_name: + return getattr(self, name) + + return None + + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: + for child in self.children: + yield from child.get_semantic_tokens() + + def get_reference(self, idx: int) -> T.Optional[LocationLink]: + for child in self.children: + if idx in child.range: + if ref := child.get_reference(idx): + return ref + return None + + @property + def document_symbol(self) -> T.Optional[DocumentSymbol]: + return None + + def get_document_symbols(self) -> T.List[DocumentSymbol]: + result = [] + for child in self.children: + if s := child.document_symbol: + s.children = child.get_document_symbols() + result.append(s) + else: + result.extend(child.get_document_symbols()) + return result + + def validate_unique_in_parent( + self, error: str, check: T.Optional[T.Callable[["AstNode"], bool]] = None + ): + for child in self.parent.children: + if child is self: + break + + if type(child) is type(self): + if check is None or check(child): + raise CompileError( + error, + references=[ + ErrorReference( + child.range, + "previous declaration was here", + ) + ], + ) + + +def validate( + token_name: T.Optional[str] = None, + end_token_name: T.Optional[str] = None, + skip_incomplete: bool = False, +): + """Decorator for functions that validate an AST node. Exceptions raised + during validation are marked with range information from the tokens.""" + + def decorator(func): + def inner(self: AstNode): + if skip_incomplete and self.incomplete: + return + + try: + func(self) + except CompileError as e: + # If the node is only partially complete, then an error must + # have already been reported at the parsing stage + if self.incomplete: + return + + if e.range is None: + e.range = ( + Range.join( + self.ranges[token_name], + self.ranges[end_token_name], + ) + or self.range + ) + + # Re-raise the exception + raise e + + inner._validator = True + return inner + + return decorator + + +class Docs: + def __init__(self, func, token_name=None): + self.func = func + self.token_name = token_name + + def __get__(self, instance, owner): + if instance is None: + return self + return self.func(instance) + + +def docs(*args, **kwargs): + """Decorator for functions that return documentation for tokens.""" + + def decorator(func): + return Docs(func, *args, **kwargs) + + return decorator + + +class Context: + def __init__(self, type: T.Type[TCtx], func: T.Callable[[AstNode], TCtx]) -> None: + self.type = type + self.func = func + + def __get__(self, instance, owner): + if instance is None: + return self + if ctx := getattr(instance, "_context_" + self.type.__name__, None): + return ctx + else: + ctx = self.func(instance) + setattr(instance, "_context_" + self.type.__name__, ctx) + return ctx + + +def context(type: T.Type[TCtx]): + """Decorator for functions that return a context object, which is passed down to .""" + + def decorator(func: T.Callable[[AstNode], TCtx]) -> Context: + return Context(type, func) + + return decorator diff --git a/gtk/blueprint-compiler/blueprintcompiler/completions.py b/gtk/blueprint-compiler/blueprintcompiler/completions.py new file mode 100644 index 00000000000..e05d6eea43f --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/completions.py @@ -0,0 +1,242 @@ +# completions.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import sys +import typing as T + +from . import gir, language +from .ast_utils import AstNode +from .completions_utils import * +from .language.types import ClassName +from .lsp_utils import Completion, CompletionItemKind +from .parser import SKIP_TOKENS +from .tokenizer import Token, TokenType + +Pattern = T.List[T.Tuple[TokenType, T.Optional[str]]] + + +def debug(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + +def _complete( + lsp, ast_node: AstNode, tokens: T.List[Token], idx: int, token_idx: int +) -> T.Iterator[Completion]: + for child in ast_node.children: + if child.group.start <= idx and ( + idx < child.group.end or (idx == child.group.end and child.incomplete) + ): + yield from _complete(lsp, child, tokens, idx, token_idx) + return + + prev_tokens: T.List[Token] = [] + + # collect the 5 previous non-skipped tokens + while len(prev_tokens) < 5 and token_idx >= 0: + token = tokens[token_idx] + if token.type not in SKIP_TOKENS: + prev_tokens.insert(0, token) + token_idx -= 1 + + for completer in ast_node.completers: + yield from completer(prev_tokens, ast_node, lsp) + + +def complete( + lsp, ast_node: AstNode, tokens: T.List[Token], idx: int +) -> T.Iterator[Completion]: + token_idx = 0 + # find the current token + for i, token in enumerate(tokens): + if token.start < idx <= token.end: + token_idx = i + + # if the current token is an identifier or whitespace, move to the token before it + while tokens[token_idx].type in [TokenType.IDENT, TokenType.WHITESPACE]: + idx = tokens[token_idx].start + token_idx -= 1 + + yield from _complete(lsp, ast_node, tokens, idx, token_idx) + + +@completer([language.GtkDirective]) +def using_gtk(lsp, ast_node, match_variables): + yield Completion( + "using Gtk 4.0", CompletionItemKind.Keyword, snippet="using Gtk 4.0;\n" + ) + + +@completer( + applies_in=[language.UI, language.ObjectContent, language.Template], + matches=new_statement_patterns, +) +def namespace(lsp, ast_node, match_variables): + yield Completion("Gtk", CompletionItemKind.Module, text="Gtk.") + for ns in ast_node.root.children[language.Import]: + if ns.gir_namespace is not None: + yield Completion( + ns.gir_namespace.name, + CompletionItemKind.Module, + text=ns.gir_namespace.name + ".", + ) + + +@completer( + applies_in=[language.UI, language.ObjectContent, language.Template], + matches=[ + [(TokenType.IDENT, None), (TokenType.OP, "."), (TokenType.IDENT, None)], + [(TokenType.IDENT, None), (TokenType.OP, ".")], + ], +) +def object_completer(lsp, ast_node, match_variables): + ns = ast_node.root.gir.namespaces.get(match_variables[0]) + if ns is not None: + for c in ns.classes.values(): + yield Completion( + c.name, + CompletionItemKind.Class, + snippet=f"{c.name} {{\n $0\n}}", + docs=c.doc, + detail=c.detail, + ) + + +@completer( + applies_in=[language.UI, language.ObjectContent, language.Template], + matches=new_statement_patterns, +) +def gtk_object_completer(lsp, ast_node, match_variables): + ns = ast_node.root.gir.namespaces.get("Gtk") + if ns is not None: + for c in ns.classes.values(): + yield Completion( + c.name, + CompletionItemKind.Class, + snippet=f"{c.name} {{\n $0\n}}", + docs=c.doc, + detail=c.detail, + ) + + +@completer( + applies_in=[language.ObjectContent], + matches=new_statement_patterns, +) +def property_completer(lsp, ast_node, match_variables): + if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): + for prop_name, prop in ast_node.gir_class.properties.items(): + if ( + isinstance(prop.type, gir.BoolType) + and lsp.client_supports_completion_choice + ): + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=f"{prop_name}: ${{1|true,false|}};", + docs=prop.doc, + detail=prop.detail, + ) + elif isinstance(prop.type, gir.StringType): + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=f'{prop_name}: "$0";', + docs=prop.doc, + detail=prop.detail, + ) + elif ( + isinstance(prop.type, gir.Enumeration) + and len(prop.type.members) <= 10 + and lsp.client_supports_completion_choice + ): + choices = ",".join(prop.type.members.keys()) + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=f"{prop_name}: ${{1|{choices}|}};", + docs=prop.doc, + detail=prop.detail, + ) + else: + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=f"{prop_name}: $0;", + docs=prop.doc, + detail=prop.detail, + ) + + +@completer( + applies_in=[language.Property, language.BaseAttribute], + matches=[[(TokenType.IDENT, None), (TokenType.OP, ":")]], +) +def prop_value_completer(lsp, ast_node, match_variables): + if (vt := ast_node.value_type) is not None: + if isinstance(vt.value_type, gir.Enumeration): + for name, member in vt.value_type.members.items(): + yield Completion( + name, + CompletionItemKind.EnumMember, + docs=member.doc, + detail=member.detail, + ) + + elif isinstance(vt.value_type, gir.BoolType): + yield Completion("true", CompletionItemKind.Constant) + yield Completion("false", CompletionItemKind.Constant) + + +@completer( + applies_in=[language.ObjectContent], + matches=new_statement_patterns, +) +def signal_completer(lsp, ast_node, match_variables): + if ast_node.gir_class and not isinstance(ast_node.gir_class, gir.ExternType): + for signal_name, signal in ast_node.gir_class.signals.items(): + if not isinstance(ast_node.parent, language.Object): + name = "on" + else: + name = "on_" + ( + ast_node.parent.children[ClassName][0].tokens["id"] + or ast_node.parent.children[ClassName][0] + .tokens["class_name"] + .lower() + ) + yield Completion( + signal_name, + CompletionItemKind.Event, + sort_text=f"1 {signal_name}", + snippet=f"{signal_name} => \\$${{1:${name}_{signal_name.replace('-', '_')}}}()$0;", + docs=signal.doc, + detail=signal.detail, + ) + + +@completer(applies_in=[language.UI], matches=new_statement_patterns) +def template_completer(lsp, ast_node, match_variables): + yield Completion( + "template", + CompletionItemKind.Snippet, + snippet="template ${1:ClassName} : ${2:ParentClass} {\n $0\n}", + ) diff --git a/gtk/blueprint-compiler/blueprintcompiler/completions_utils.py b/gtk/blueprint-compiler/blueprintcompiler/completions_utils.py new file mode 100644 index 00000000000..03bec0f99e7 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/completions_utils.py @@ -0,0 +1,86 @@ +# completions_utils.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +import typing as T + +from .lsp_utils import Completion +from .tokenizer import Token, TokenType + +new_statement_patterns = [ + [(TokenType.PUNCTUATION, "{")], + [(TokenType.PUNCTUATION, "}")], + [(TokenType.PUNCTUATION, "]")], + [(TokenType.PUNCTUATION, ";")], +] + + +def applies_to(*ast_types): + """Decorator describing which AST nodes the completer should apply in.""" + + def decorator(func): + for c in ast_types: + c.completers.append(func) + return func + + return decorator + + +def completer(applies_in: T.List, matches: T.List = [], applies_in_subclass=None): + def decorator(func): + def inner(prev_tokens: T.List[Token], ast_node, lsp): + # For completers that apply in ObjectContent nodes, we can further + # check that the object is the right class + if applies_in_subclass is not None: + type = ast_node.root.gir.get_type( + applies_in_subclass[1], applies_in_subclass[0] + ) + if not ast_node.gir_class or not ast_node.gir_class.assignable_to(type): + return + + any_match = len(matches) == 0 + match_variables: T.List[str] = [] + + for pattern in matches: + match_variables = [] + + if len(pattern) <= len(prev_tokens): + for i in range(0, len(pattern)): + type, value = pattern[i] + token = prev_tokens[i - len(pattern)] + if token.type != type or ( + value is not None and str(token) != value + ): + break + if value is None: + match_variables.append(str(token)) + else: + any_match = True + break + + if not any_match: + return + + yield from func(lsp, ast_node, match_variables) + + for c in applies_in: + c.completers.append(inner) + return inner + + return decorator diff --git a/gtk/blueprint-compiler/blueprintcompiler/decompiler.py b/gtk/blueprint-compiler/blueprintcompiler/decompiler.py new file mode 100644 index 00000000000..de6c06f4031 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/decompiler.py @@ -0,0 +1,475 @@ +# decompiler.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import typing as T +from collections import defaultdict +from dataclasses import dataclass +from enum import Enum + +from . import formatter +from .gir import * +from .utils import Colors, escape_quote +from .xml_reader import Element, parse, parse_string + +__all__ = ["decompile"] + + +_DECOMPILERS: dict[str, list] = defaultdict(list) +_CLOSING = { + "{": "}", + "[": "]", +} +_NAMESPACES = [ + ("GLib", "2.0"), + ("GObject", "2.0"), + ("Gio", "2.0"), + ("Adw", "1"), +] + + +class LineType(Enum): + NONE = 1 + STMT = 2 + BLOCK_START = 3 + BLOCK_END = 4 + + +class DecompileCtx: + def __init__(self, parent_gir: T.Optional[GirContext] = None) -> None: + self.sub_decompiler = parent_gir is not None + self._result: str = "" + self.gir = parent_gir or GirContext() + self._blocks_need_end: T.List[str] = [] + self._last_line_type: LineType = LineType.NONE + self._obj_type_stack: list[T.Optional[GirType]] = [] + self._node_stack: list[Element] = [] + + self.gir.add_namespace(get_namespace("Gtk", "4.0")) + + @property + def result(self) -> str: + imports = "" + + if not self.sub_decompiler: + import_lines = sorted( + [ + f"using {ns} {namespace.version};" + for ns, namespace in self.gir.namespaces.items() + if ns != "Gtk" + ] + ) + imports += "\n".join(["using Gtk 4.0;", *import_lines]) + + return formatter.format(imports + self._result) + + def type_by_cname(self, cname: str) -> T.Optional[GirType]: + if type := self.gir.get_type_by_cname(cname): + return type + + for ns, version in _NAMESPACES: + try: + namespace = get_namespace(ns, version) + if type := namespace.get_type_by_cname(cname): + self.gir.add_namespace(namespace) + return type + except: + pass + + return None + + def start_block(self) -> None: + self._blocks_need_end.append("") + self._obj_type_stack.append(None) + + def end_block(self) -> None: + if close := self._blocks_need_end.pop(): + self.print(close) + self._obj_type_stack.pop() + + @property + def current_obj_type(self) -> T.Optional[GirType]: + return next((x for x in reversed(self._obj_type_stack) if x is not None), None) + + def push_obj_type(self, type: T.Optional[GirType]) -> None: + self._obj_type_stack[-1] = type + + @property + def current_node(self) -> T.Optional[Element]: + if len(self._node_stack) == 0: + return None + else: + return self._node_stack[-1] + + @property + def parent_node(self) -> T.Optional[Element]: + if len(self._node_stack) < 2: + return None + else: + return self._node_stack[-2] + + @property + def root_node(self) -> T.Optional[Element]: + if len(self._node_stack) == 0: + return None + else: + return self._node_stack[0] + + @property + def template_class(self) -> T.Optional[str]: + assert self.root_node is not None + for child in self.root_node.children: + if child.tag == "template": + return child["class"] + + return None + + def find_object(self, id: str) -> T.Optional[Element]: + assert self.root_node is not None + for child in self.root_node.children: + if child.tag == "template" and child["class"] == id: + return child + + def find_in_children(node: Element) -> T.Optional[Element]: + if node.tag in ["object", "menu"] and node["id"] == id: + return node + else: + for child in node.children: + if result := find_in_children(child): + return result + return None + + return find_in_children(self.root_node) + + def end_block_with(self, text: str) -> None: + self._blocks_need_end[-1] = text + + def print(self, line: str, newline: bool = True) -> None: + self._result += line + + if line.endswith("{") or line.endswith("["): + if len(self._blocks_need_end): + self._blocks_need_end[-1] = _CLOSING[line[-1]] + + # Converts a value from an XML element to a blueprint string + # based on the given type. Returns a tuple of translator comments + # (if any) and the decompiled syntax. + def decompile_value( + self, + value: str, + type: T.Optional[GirType], + translatable: T.Optional[T.Tuple[str, str, str]] = None, + ) -> T.Tuple[str, str]: + def get_enum_name(value): + for member in type.members.values(): + if ( + member.nick == value + or member.c_ident == value + or str(member.value) == value + ): + return member.name + return value.replace("-", "_") + + if translatable is not None and truthy(translatable[0]): + return decompile_translatable(value, *translatable) + elif type is None: + return "", f"{escape_quote(value)}" + elif type.assignable_to(FloatType()): + return "", str(value) + elif type.assignable_to(BoolType()): + val = truthy(value) + return "", ("true" if val else "false") + elif type.assignable_to(ArrayType(StringType())): + items = ", ".join([escape_quote(x) for x in value.split("\n")]) + return "", f"[{items}]" + elif ( + type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf")) + or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture")) + or type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable") + ) + or type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction") + ) + or type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger") + ) + ): + return "", escape_quote(value) + elif value == self.template_class: + return "", "template" + elif type.assignable_to( + self.gir.namespaces["Gtk"].lookup_type("GObject.Object") + ) or isinstance(type, Interface): + return "", ("null" if value == "" else value) + elif isinstance(type, Bitfield): + flags = [get_enum_name(flag) for flag in value.split("|")] + return "", " | ".join(flags) + elif isinstance(type, Enumeration): + return "", get_enum_name(value) + elif isinstance(type, TypeType): + if t := self.type_by_cname(value): + return "", f"typeof<{full_name(t)}>" + else: + return "", f"typeof<${value}>" + else: + return "", escape_quote(value) + + +def decompile_element( + ctx: DecompileCtx, gir: T.Optional[GirContext], xml: Element +) -> None: + try: + decompilers = [d for d in _DECOMPILERS[xml.tag] if d._filter(ctx)] + if len(decompilers) == 0: + raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>") + + decompiler = decompilers[0] + + if decompiler._element: + args = [ctx, gir, xml] + kwargs: T.Dict[str, T.Optional[str]] = {} + else: + args = [ctx, gir] + kwargs = {canon(name): value for name, value in xml.attrs.items()} + if decompiler._cdata: + if len(xml.children): + kwargs["cdata"] = None + else: + kwargs["cdata"] = xml.cdata + + ctx._node_stack.append(xml) + ctx.start_block() + gir = decompiler(*args, **kwargs) + + if not decompiler._skip_children: + for child in xml.children: + decompile_element(ctx, gir, child) + + ctx.end_block() + ctx._node_stack.pop() + + except UnsupportedError as e: + raise e + except TypeError as e: + raise UnsupportedError(tag=xml.tag) + + +def decompile(data: str) -> str: + ctx = DecompileCtx() + + xml = parse(data) + decompile_element(ctx, None, xml) + + return ctx.result + + +def decompile_string(data: str) -> str: + ctx = DecompileCtx() + + xml = parse_string(data) + decompile_element(ctx, None, xml) + + return ctx.result + + +def canon(string: str) -> str: + if string == "class": + return "klass" + else: + return string.replace("-", "_").lower() + + +def truthy(string: str) -> bool: + return string is not None and string.lower() in ["yes", "true", "t", "y", "1"] + + +def full_name(gir: GirType) -> str: + return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name + + +def lookup_by_cname(gir, cname: str) -> T.Optional[GirType]: + if isinstance(gir, GirContext): + return gir.get_type_by_cname(cname) + else: + return gir.get_containing(Repository).get_type_by_cname(cname) + + +def decompiler( + tag, + cdata=False, + parent_type: T.Optional[str] = None, + parent_tag: T.Optional[str] = None, + skip_children=False, + element=False, +): + def decorator(func): + func._cdata = cdata + func._skip_children = skip_children + func._element = element + + def filter(ctx): + if parent_type is not None: + if ( + ctx.current_obj_type is None + or ctx.current_obj_type.full_name != parent_type + ): + return False + + if parent_tag is not None: + if not any(x.tag == parent_tag for x in ctx._node_stack): + return False + + return True + + func._filter = filter + + _DECOMPILERS[tag].append(func) + return func + + return decorator + + +@decompiler("interface") +def decompile_interface(ctx, gir, domain=None): + if domain is not None: + ctx.print(f"translation-domain {escape_quote(domain)};") + return gir + + +@decompiler("requires") +def decompile_requires(ctx, gir, lib=None, version=None): + return gir + + +@decompiler("placeholder") +def decompile_placeholder(ctx, gir): + pass + + +def decompile_translatable( + string: str, + translatable: T.Optional[str], + context: T.Optional[str], + comments: T.Optional[str], +) -> T.Tuple[str, str]: + if translatable is not None and truthy(translatable): + if comments is None: + comments = "" + else: + comments = comments.replace("/*", " ").replace("*/", " ") + comments = f"/* Translators: {comments} */" + + if context is not None: + return comments, f"C_({escape_quote(context)}, {escape_quote(string)})" + else: + return comments, f"_({escape_quote(string)})" + else: + return "", f"{escape_quote(string)}" + + +@decompiler("property", cdata=True) +def decompile_property( + ctx: DecompileCtx, + gir, + name, + cdata, + bind_source=None, + bind_property=None, + bind_flags=None, + translatable="false", + comments=None, + context=None, +): + name = name.replace("_", "-") + if cdata is None: + ctx.print(f"{name}: ") + ctx.end_block_with(";") + elif bind_source: + flags = "" + bind_flags = bind_flags or [] + if "sync-create" not in bind_flags: + flags += " no-sync-create" + if "invert-boolean" in bind_flags: + flags += " inverted" + if "bidirectional" in bind_flags: + flags += " bidirectional" + + if bind_source == ctx.template_class: + bind_source = "template" + + ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};") + elif truthy(translatable): + comments, translatable = decompile_translatable( + cdata, translatable, context, comments + ) + if comments is not None: + ctx.print(comments) + ctx.print(f"{name}: {translatable};") + elif gir is None or gir.properties.get(name) is None: + ctx.print(f"{name}: {escape_quote(cdata)};") + elif ( + gir.assignable_to(ctx.gir.get_class("BuilderListItemFactory", "Gtk")) + and name == "bytes" + ): + sub_ctx = DecompileCtx(ctx.gir) + + xml = parse_string(cdata) + decompile_element(sub_ctx, None, xml) + + ctx.print(sub_ctx.result) + else: + _, string = ctx.decompile_value(cdata, gir.properties.get(name).type) + ctx.print(f"{name}: {string};") + return gir + + +@decompiler("attribute", cdata=True) +def decompile_attribute( + ctx, gir, name, cdata, translatable="false", comments=None, context=None +): + decompile_property( + ctx, + gir, + name, + cdata, + translatable=translatable, + comments=comments, + context=context, + ) + + +@decompiler("attributes") +def decompile_attributes(ctx, gir): + ctx.print("attributes {") + + +@dataclass +class UnsupportedError(Exception): + message: str = "unsupported feature" + tag: T.Optional[str] = None + + def print(self, filename: str): + print(f"\n{Colors.RED}{Colors.BOLD}error: {self.message}{Colors.CLEAR}") + print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}") + if self.tag: + print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}") + print( + f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You +probably need to port this file manually.{Colors.CLEAR}\n""" + ) diff --git a/gtk/blueprint-compiler/blueprintcompiler/errors.py b/gtk/blueprint-compiler/blueprintcompiler/errors.py new file mode 100644 index 00000000000..1e7297cccef --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/errors.py @@ -0,0 +1,226 @@ +# errors.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import sys +import traceback +import typing as T +from dataclasses import dataclass + +from . import utils +from .tokenizer import Range +from .utils import Colors + + +class PrintableError(Exception): + """Parent class for errors that can be pretty-printed for the user, e.g. + compilation warnings and errors.""" + + def pretty_print(self, filename, code, stream=sys.stdout): + raise NotImplementedError() + + +@dataclass +class ErrorReference: + range: Range + message: str + + +class CompileError(PrintableError): + """A PrintableError with a start/end position and optional hints""" + + category = "error" + color = Colors.RED + + def __init__( + self, + message: str, + range: T.Optional[Range] = None, + did_you_mean: T.Optional[T.Tuple[str, T.List[str]]] = None, + hints: T.Optional[T.List[str]] = None, + actions: T.Optional[T.List["CodeAction"]] = None, + fatal: bool = False, + references: T.Optional[T.List[ErrorReference]] = None, + ) -> None: + super().__init__(message) + + self.message = message + self.range = range + self.hints = hints or [] + self.actions = actions or [] + self.references = references or [] + self.fatal = fatal + + if did_you_mean is not None: + self._did_you_mean(*did_you_mean) + + def hint(self, hint: str) -> "CompileError": + self.hints.append(hint) + return self + + def _did_you_mean(self, word: str, options: T.List[str]) -> None: + if word.replace("_", "-") in options: + self.hint(f"use '-', not '_': `{word.replace('_', '-')}`") + return + + recommend = utils.did_you_mean(word, options) + if recommend is not None: + if word.casefold() == recommend.casefold(): + self.hint(f"Did you mean `{recommend}` (note the capitalization)?") + else: + self.hint(f"Did you mean `{recommend}`?") + self.actions.append(CodeAction(f"Change to `{recommend}`", recommend)) + else: + self.hint("Did you check your spelling?") + self.hint("Are your dependencies up to date?") + + def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None: + assert self.range is not None + + line_num, col_num = utils.idx_to_pos(self.range.start + 1, code) + end_line_num, end_col_num = utils.idx_to_pos(self.range.end + 1, code) + line = code.splitlines(True)[line_num] if code != "" else "" + + # Display 1-based line numbers + line_num += 1 + end_line_num += 1 + + n_spaces = col_num - 1 + n_carets = ( + (end_col_num - col_num) + if line_num == end_line_num + else (len(line) - n_spaces - 1) + ) + + n_spaces += line.count("\t", 0, col_num) + n_carets += line.count("\t", col_num, col_num + n_carets) + line = line.replace("\t", " ") + + stream.write( + f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR} +at {filename} line {line_num} column {col_num}: +{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*n_spaces}{"^"*n_carets}{Colors.CLEAR}\n""" + ) + + for hint in self.hints: + stream.write(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}\n") + + for i, action in enumerate(self.actions): + old = ( + action.edit_range.text + if action.edit_range is not None + else self.range.text + ) + + if old == "": + stream.write( + f"suggestion: insert {Colors.GREEN}{action.replace_with}{Colors.CLEAR}\n" + ) + elif action.replace_with == "": + stream.write(f"suggestion: remove {Colors.RED}{old}{Colors.CLEAR}\n") + else: + stream.write( + f"suggestion: replace {Colors.RED}{old}{Colors.CLEAR} with {Colors.GREEN}{action.replace_with}{Colors.CLEAR}\n" + ) + + for ref in self.references: + line_num, col_num = utils.idx_to_pos(ref.range.start + 1, code) + line = code.splitlines(True)[line_num] + line_num += 1 + + stream.write( + f"""{Colors.FAINT}note: {ref.message}: +at {filename} line {line_num} column {col_num}: +{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n""" + ) + + stream.write("\n") + + +class CompileWarning(CompileError): + category = "warning" + color = Colors.YELLOW + + +class DeprecatedWarning(CompileWarning): + pass + + +class UnusedWarning(CompileWarning): + pass + + +class UpgradeWarning(CompileWarning): + category = "upgrade" + color = Colors.PURPLE + + +class UnexpectedTokenError(CompileError): + def __init__(self, range: Range) -> None: + super().__init__("Unexpected tokens", range) + + +@dataclass +class CodeAction: + title: str + replace_with: str + edit_range: T.Optional[Range] = None + + +class MultipleErrors(PrintableError): + """If multiple errors occur during compilation, they can be collected into + a list and re-thrown using the MultipleErrors exception. It will + pretty-print all of the errors and a count of how many errors there are.""" + + def __init__(self, errors: T.List[CompileError]) -> None: + super().__init__() + self.errors = errors + + def pretty_print(self, filename, code, stream=sys.stdout) -> None: + for error in self.errors: + error.pretty_print(filename, code, stream) + if len(self.errors) != 1: + print(f"{len(self.errors)} errors") + + +class CompilerBugError(Exception): + """Emitted on assertion errors""" + + +def assert_true(truth: bool, message: T.Optional[str] = None): + if not truth: + raise CompilerBugError(message) + + +def report_bug(): # pragma: no cover + """Report an error and ask people to report it.""" + + from . import main + + print(traceback.format_exc()) + print(f"Arguments: {sys.argv}") + print(f"Version: {main.VERSION}\n") + print( + f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG ***** +The blueprint-compiler program has crashed. Please report the above stacktrace, +along with the input file(s) if possible, on GitLab: +{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue +{Colors.CLEAR}""" + ) + + sys.exit(1) diff --git a/gtk/blueprint-compiler/blueprintcompiler/formatter.py b/gtk/blueprint-compiler/blueprintcompiler/formatter.py new file mode 100644 index 00000000000..c003d458cd8 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/formatter.py @@ -0,0 +1,226 @@ +# formatter.py +# +# Copyright 2023 Gregor Niehl +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import re +from enum import Enum + +from . import tokenizer, utils +from .tokenizer import TokenType + +OPENING_TOKENS = ("{", "[") +CLOSING_TOKENS = ("}", "]") + +NEWLINE_AFTER = tuple(";") + OPENING_TOKENS + CLOSING_TOKENS + +NO_WHITESPACE_BEFORE = (",", ":", "::", ";", ")", ".", ">", "]", "=") +NO_WHITESPACE_AFTER = ("C_", "_", "(", ".", "$", "<", "::", "[", "=") + +# NO_WHITESPACE_BEFORE takes precedence over WHITESPACE_AFTER +WHITESPACE_AFTER = (":", ",", ">", ")", "|", "=>") +WHITESPACE_BEFORE = ("{", "|") + + +class LineType(Enum): + STATEMENT = 0 + BLOCK_OPEN = 1 + BLOCK_CLOSE = 2 + CHILD_TYPE = 3 + COMMENT = 4 + + +def format(data, tab_size=2, insert_space=True): + indent_levels = 0 + tokens = tokenizer.tokenize(data) + end_str = "" + last_not_whitespace = tokens[0] + current_line = "" + prev_line_type = None + is_child_type = False + indent_item = " " * tab_size if insert_space else "\t" + watch_parentheses = False + parentheses_balance = 0 + bracket_tracker = [None] + last_whitespace_contains_newline = False + + def commit_current_line( + line_type=prev_line_type, redo_whitespace=False, newlines_before=1 + ): + nonlocal end_str, current_line, prev_line_type + + indent_whitespace = indent_levels * indent_item + whitespace_to_add = "\n" + indent_whitespace + + if redo_whitespace or newlines_before != 1: + end_str = end_str.strip() + "\n" * newlines_before + if newlines_before > 0: + end_str += indent_whitespace + + end_str += current_line + whitespace_to_add + + current_line = "" + prev_line_type = line_type + + for item in tokens: + str_item = str(item) + + if item.type == TokenType.WHITESPACE: + last_whitespace_contains_newline = "\n" in str_item + continue + + whitespace_required = ( + str_item in WHITESPACE_BEFORE + or str(last_not_whitespace) in WHITESPACE_AFTER + or (str_item == "(" and end_str.endswith(": bind")) + ) + whitespace_blockers = ( + str_item in NO_WHITESPACE_BEFORE + or str(last_not_whitespace) in NO_WHITESPACE_AFTER + or (str_item == "<" and str(last_not_whitespace) == "typeof") + ) + + this_or_last_is_ident = TokenType.IDENT in (item.type, last_not_whitespace.type) + current_line_is_empty = len(current_line) == 0 + is_function = str_item == "(" and not re.match( + r"^([A-Za-z_\-])+(: bind)?$", current_line + ) + + any_blockers = whitespace_blockers or current_line_is_empty or is_function + if (whitespace_required or this_or_last_is_ident) and not any_blockers: + current_line += " " + + current_line += str_item + + if str_item in ("[", "("): + bracket_tracker.append(str_item) + elif str_item in ("]", ")"): + bracket_tracker.pop() + + needs_newline_treatment = ( + str_item in NEWLINE_AFTER or item.type == TokenType.COMMENT + ) + if needs_newline_treatment: + if str_item in OPENING_TOKENS: + list_or_child_type = str_item == "[" + if list_or_child_type: + is_child_type = current_line.startswith("[") + + if is_child_type: + if str(last_not_whitespace) not in OPENING_TOKENS: + end_str = ( + end_str.strip() + "\n\n" + (indent_item * indent_levels) + ) + last_not_whitespace = item + continue + + indent_levels += 1 + keep_same_indent = prev_line_type not in ( + LineType.CHILD_TYPE, + LineType.COMMENT, + LineType.BLOCK_OPEN, + ) + if keep_same_indent: + end_str = ( + end_str.strip() + "\n\n" + indent_item * (indent_levels - 1) + ) + commit_current_line(LineType.BLOCK_OPEN) + + elif str_item == "]" and is_child_type: + commit_current_line(LineType.CHILD_TYPE, False) + is_child_type = False + + elif str_item in CLOSING_TOKENS: + if str_item == "]" and last_not_whitespace != ",": + current_line = current_line[:-1] + commit_current_line() + current_line = "]" + elif str(last_not_whitespace) in OPENING_TOKENS: + end_str = end_str.strip() + commit_current_line(LineType.BLOCK_CLOSE, True, 0) + + indent_levels -= 1 + commit_current_line(LineType.BLOCK_CLOSE, True) + + elif str_item == ";": + line_type = LineType.STATEMENT + newlines = 1 + + if len(current_line) == 1: + newlines = 0 + line_type = LineType.BLOCK_CLOSE + elif prev_line_type == LineType.BLOCK_CLOSE: + newlines = 2 + + commit_current_line(line_type, newlines_before=newlines) + + elif item.type == TokenType.COMMENT: + require_extra_newline = ( + LineType.BLOCK_CLOSE, + LineType.STATEMENT, + LineType.COMMENT, + ) + + single_line_comment = str_item.startswith("//") + newlines = 1 + if single_line_comment: + if not str_item.startswith("// "): + current_line = f"// {current_line[2:]}" + + if not last_whitespace_contains_newline: + current_line = " " + current_line + newlines = 0 + elif prev_line_type == LineType.BLOCK_CLOSE: + newlines = 2 + + elif prev_line_type in require_extra_newline: + newlines = 2 + + commit_current_line(LineType.COMMENT, newlines_before=newlines) + + else: + commit_current_line() + + elif str_item == "(" and ( + re.match(r"^([A-Za-z_\-])+\s*\(", current_line) or watch_parentheses + ): + watch_parentheses = True + parentheses_balance += 1 + + elif str_item == ")" and watch_parentheses: + parentheses_balance -= 1 + all_parentheses_closed = parentheses_balance == 0 + if all_parentheses_closed: + commit_current_line( + newlines_before=2 if prev_line_type == LineType.BLOCK_CLOSE else 1 + ) + watch_parentheses = False + + tracker_is_empty = len(bracket_tracker) > 0 + if tracker_is_empty: + last_in_tracker = bracket_tracker[-1] + is_list_comma = last_in_tracker == "[" and str_item == "," + if is_list_comma: + last_was_list_item = end_str.strip()[-1] not in ("[", ",") + if last_was_list_item: + end_str = end_str.strip() + commit_current_line() + + last_not_whitespace = item + last_whitespace_contains_newline = False + + return end_str.strip() + "\n" diff --git a/gtk/blueprint-compiler/blueprintcompiler/gir.py b/gtk/blueprint-compiler/blueprintcompiler/gir.py new file mode 100644 index 00000000000..30a5eaa3743 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/gir.py @@ -0,0 +1,1084 @@ +# gir.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import os +import sys +import typing as T +from functools import cached_property + +import gi # type: ignore + +gi.require_version("GIRepository", "2.0") +from gi.repository import GIRepository # type: ignore + +from . import typelib, xml_reader +from .errors import CompileError, CompilerBugError +from .lsp_utils import CodeAction + +_namespace_cache: T.Dict[str, "Namespace"] = {} +_xml_cache = {} + +_user_search_paths = [] + + +def add_typelib_search_path(path: str): + _user_search_paths.append(path) + + +def get_namespace(namespace: str, version: str) -> "Namespace": + search_paths = [*GIRepository.Repository.get_search_path(), *_user_search_paths] + + filename = f"{namespace}-{version}.typelib" + + if filename not in _namespace_cache: + for search_path in search_paths: + path = os.path.join(search_path, filename) + + if os.path.exists(path) and os.path.isfile(path): + tl = typelib.load_typelib(path) + repository = Repository(tl) + + _namespace_cache[filename] = repository.namespace + break + + if filename not in _namespace_cache: + raise CompileError( + f"Namespace {namespace}-{version} could not be found", + hints=["search path: " + os.pathsep.join(search_paths)], + ) + + return _namespace_cache[filename] + + +_available_namespaces: list[tuple[str, str]] = [] + + +def get_available_namespaces() -> T.List[T.Tuple[str, str]]: + if len(_available_namespaces): + return _available_namespaces + + search_paths: list[str] = [ + *GIRepository.Repository.get_search_path(), + *_user_search_paths, + ] + + for search_path in search_paths: + try: + filenames = os.listdir(search_path) + except FileNotFoundError: + continue + + for filename in filenames: + if filename.endswith(".typelib"): + namespace, version = filename.removesuffix(".typelib").rsplit("-", 1) + _available_namespaces.append((namespace, version)) + + return _available_namespaces + + +def get_xml(namespace: str, version: str): + search_paths = [] + + if data_paths := os.environ.get("XDG_DATA_DIRS"): + search_paths += [ + os.path.join(path, "gir-1.0") for path in data_paths.split(os.pathsep) + ] + + filename = f"{namespace}-{version}.gir" + + if filename not in _xml_cache: + for search_path in search_paths: + path = os.path.join(search_path, filename) + + if os.path.exists(path) and os.path.isfile(path): + _xml_cache[filename] = xml_reader.parse(path) + break + + if filename not in _xml_cache: + raise CompileError( + f"GObject introspection file '{namespace}-{version}.gir' could not be found", + hints=["search path: " + os.pathsep.join(search_paths)], + ) + + return _xml_cache[filename] + + +ONLINE_DOCS = { + "Adw-1": "https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/", + "Gdk-4.0": "https://docs.gtk.org/gdk4/", + "GdkPixbuf-2.0": "https://docs.gtk.org/gdk-pixbuf/", + "Gio-2.0": "https://docs.gtk.org/gio/", + "GLib-2.0": "https://docs.gtk.org/glib/", + "GModule-2.0": "https://docs.gtk.org/gmodule/", + "GObject-2.0": "https://docs.gtk.org/gobject/", + "Gsk-4.0": "https://docs.gtk.org/gsk4/", + "Gtk-4.0": "https://docs.gtk.org/gtk4/", + "GtkSource-5": "https://gnome.pages.gitlab.gnome.org/gtksourceview/gtksourceview5", + "Pango-1.0": "https://docs.gtk.org/Pango/", + "Shumate-1.0": "https://gnome.pages.gitlab.gnome.org/libshumate/", + "WebKit2-4.1": "https://webkitgtk.org/reference/webkit2gtk/stable/", +} + + +class GirType: + @property + def doc(self) -> T.Optional[str]: + return None + + def assignable_to(self, other: "GirType") -> bool: + raise NotImplementedError() + + @property + def name(self) -> str: + """The GIR name of the type, not including the namespace""" + raise NotImplementedError() + + @property + def full_name(self) -> str: + """The GIR name of the type to use in diagnostics""" + raise NotImplementedError() + + @property + def glib_type_name(self) -> str: + """The name of the type in the GObject type system, suitable to pass to `g_type_from_name()`.""" + raise NotImplementedError() + + @property + def incomplete(self) -> bool: + return False + + @property + def deprecated(self) -> bool: + return False + + @property + def deprecated_doc(self) -> T.Optional[str]: + return None + + +class ExternType(GirType): + def __init__(self, name: str) -> None: + super().__init__() + self._name = name + + def assignable_to(self, other: GirType) -> bool: + return True + + @property + def full_name(self) -> str: + return self._name + + @property + def glib_type_name(self) -> str: + return self._name + + @property + def incomplete(self) -> bool: + return True + + +class ArrayType(GirType): + def __init__(self, inner: GirType) -> None: + self._inner = inner + + def assignable_to(self, other: GirType) -> bool: + return isinstance(other, ArrayType) and self._inner.assignable_to(other._inner) + + @property + def inner(self) -> GirType: + return self._inner + + @property + def name(self) -> str: + return self._inner.name + "[]" + + @property + def full_name(self) -> str: + return self._inner.full_name + "[]" + + +class BasicType(GirType): + name: str = "unknown type" + + @property + def full_name(self) -> str: + return self.name + + +class BoolType(BasicType): + name = "bool" + glib_type_name: str = "gboolean" + + def assignable_to(self, other: GirType) -> bool: + return isinstance(other, BoolType) + + +class IntType(BasicType): + name = "int" + glib_type_name: str = "gint" + + def assignable_to(self, other: GirType) -> bool: + return ( + isinstance(other, IntType) + or isinstance(other, UIntType) + or isinstance(other, FloatType) + ) + + +class UIntType(BasicType): + name = "uint" + glib_type_name: str = "guint" + + def assignable_to(self, other: GirType) -> bool: + return ( + isinstance(other, IntType) + or isinstance(other, UIntType) + or isinstance(other, FloatType) + ) + + +class FloatType(BasicType): + name = "float" + glib_type_name: str = "gfloat" + + def assignable_to(self, other: GirType) -> bool: + return isinstance(other, FloatType) + + +class StringType(BasicType): + name = "string" + glib_type_name: str = "gchararray" + + def assignable_to(self, other: GirType) -> bool: + return isinstance(other, StringType) + + +class TypeType(BasicType): + name = "GType" + glib_type_name: str = "GType" + + def assignable_to(self, other: GirType) -> bool: + return isinstance(other, TypeType) + + +_BASIC_TYPES = { + "bool": BoolType, + "string": StringType, + "int": IntType, + "uint": UIntType, + "float": FloatType, + "double": FloatType, + "type": TypeType, +} + + +TNode = T.TypeVar("TNode", bound="GirNode") + + +class GirNode: + xml_tag: str + + def __init__(self, container: T.Optional["GirNode"], tl: typelib.Typelib) -> None: + self.container = container + self.tl = tl + + def get_containing(self, container_type: T.Type[TNode]) -> TNode: + if self.container is None: + raise CompilerBugError() + elif isinstance(self.container, container_type): + return self.container + else: + return self.container.get_containing(container_type) + + @cached_property + def xml(self): + for el in self.container.xml.children: + if el.attrs.get("name") == self.name: + if el.tag == self.xml_tag: + return el + + @cached_property + def glib_type_name(self) -> str: + return self.tl.OBJ_GTYPE_NAME + + @cached_property + def full_name(self) -> str: + if self.container is None: + return self.name + else: + return f"{self.container.name}.{self.name}" + + @cached_property + def name(self) -> str: + return self.tl.BLOB_NAME + + @cached_property + def cname(self) -> str: + return self.tl.OBJ_GTYPE_NAME + + @cached_property + def available_in(self) -> str: + return self.xml.get("version") + + @cached_property + def detail(self) -> T.Optional[str]: + try: + el = self.xml.get_elements("doc") + if len(el) == 1: + return el[0].cdata.strip().partition("\n")[0] + else: + return None + except: + return None + + @cached_property + def doc(self) -> T.Optional[str]: + sections = [] + + if self.signature: + sections.append("```\n" + self.signature + "\n```") + + try: + el = self.xml.get_elements("doc") + if len(el) == 1: + sections.append(el[0].cdata.strip()) + except: + # Not a huge deal, but if you want docs in the language server you + # should ensure .gir files are installed + sections.append("Documentation is not installed") + + if self.online_docs: + sections.append(f"[Online documentation]({self.online_docs})") + + return "\n\n---\n\n".join(sections) + + @property + def online_docs(self) -> T.Optional[str]: + return None + + @property + def signature(self) -> T.Optional[str]: + return None + + @property + def type(self) -> GirType: + raise NotImplementedError() + + @property + def deprecated_doc(self) -> T.Optional[str]: + try: + return self.xml.get_elements("doc-deprecated")[0].cdata.strip() + except: + return None + + +class Property(GirNode): + xml_tag = "property" + + def __init__(self, klass: T.Union["Class", "Interface"], tl: typelib.Typelib): + super().__init__(klass, tl) + + @cached_property + def name(self) -> str: + return self.tl.PROP_NAME + + @cached_property + def type(self): + return self.get_containing(Repository)._resolve_type_id(self.tl.PROP_TYPE) + + @cached_property + def signature(self): + return f"{self.type.full_name} {self.container.name}:{self.name}" + + @property + def writable(self) -> bool: + return self.tl.PROP_WRITABLE == 1 + + @property + def construct_only(self) -> bool: + return self.tl.PROP_CONSTRUCT_ONLY == 1 + + @property + def online_docs(self) -> T.Optional[str]: + if ns := self.get_containing(Namespace).online_docs: + assert self.container is not None + return f"{ns}property.{self.container.name}.{self.name}.html" + else: + return None + + @property + def deprecated(self) -> bool: + return self.tl.PROP_DEPRECATED == 1 + + +class Argument(GirNode): + def __init__(self, container: GirNode, tl: typelib.Typelib) -> None: + super().__init__(container, tl) + + @cached_property + def name(self) -> str: + return self.tl.ARG_NAME + + @cached_property + def type(self) -> GirType: + return self.get_containing(Repository)._resolve_type_id(self.tl.ARG_TYPE) + + +class Signature(GirNode): + def __init__(self, container: GirNode, tl: typelib.Typelib) -> None: + super().__init__(container, tl) + + @cached_property + def args(self) -> T.List[Argument]: + n_arguments = self.tl.SIGNATURE_N_ARGUMENTS + blob_size = self.tl.header.HEADER_ARG_BLOB_SIZE + result = [] + for i in range(n_arguments): + entry = self.tl.SIGNATURE_ARGUMENTS[i * blob_size] + result.append(Argument(self, entry)) + return result + + @cached_property + def return_type(self) -> GirType: + return self.get_containing(Repository)._resolve_type_id( + self.tl.SIGNATURE_RETURN_TYPE + ) + + +class Signal(GirNode): + xml_tag = "glib:signal" + + def __init__( + self, klass: T.Union["Class", "Interface"], tl: typelib.Typelib + ) -> None: + super().__init__(klass, tl) + + @cached_property + def gir_signature(self) -> Signature: + return Signature(self, self.tl.SIGNAL_SIGNATURE) + + @property + def signature(self): + args = ", ".join( + [f"{a.type.full_name} {a.name}" for a in self.gir_signature.args] + ) + return f"signal {self.container.full_name}::{self.name} ({args})" + + @property + def online_docs(self) -> T.Optional[str]: + if ns := self.get_containing(Namespace).online_docs: + assert self.container is not None + return f"{ns}signal.{self.container.name}.{self.name}.html" + else: + return None + + @property + def deprecated(self) -> bool: + return self.tl.SIGNAL_DEPRECATED == 1 + + +class Interface(GirNode, GirType): + xml_tag = "interface" + + def __init__(self, ns: "Namespace", tl: typelib.Typelib): + super().__init__(ns, tl) + + @cached_property + def properties(self) -> T.Mapping[str, Property]: + n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES + offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE + offset += (n_prerequisites + n_prerequisites % 2) * 2 + n_properties = self.tl.INTERFACE_N_PROPERTIES + property_size = self.tl.header.HEADER_PROPERTY_BLOB_SIZE + result = {} + for i in range(n_properties): + property = Property(self, self.tl[offset + i * property_size]) + result[property.name] = property + return result + + @cached_property + def signals(self) -> T.Mapping[str, Signal]: + n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES + offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE + offset += (n_prerequisites + n_prerequisites % 2) * 2 + offset += ( + self.tl.INTERFACE_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE + ) + offset += self.tl.INTERFACE_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE + n_signals = self.tl.INTERFACE_N_SIGNALS + property_size = self.tl.header.HEADER_SIGNAL_BLOB_SIZE + result = {} + for i in range(n_signals): + signal = Signal(self, self.tl[offset + i * property_size]) + result[signal.name] = signal + return result + + @cached_property + def prerequisites(self) -> T.List["Interface"]: + n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES + result = [] + for i in range(n_prerequisites): + entry = self.tl.INTERFACE_PREREQUISITES[i * 2].AS_DIR_ENTRY + result.append(self.get_containing(Repository)._resolve_dir_entry(entry)) + return result + + def assignable_to(self, other: GirType) -> bool: + if self == other: + return True + for pre in self.prerequisites: + if pre.assignable_to(other): + return True + return False + + @property + def online_docs(self) -> T.Optional[str]: + if ns := self.get_containing(Namespace).online_docs: + return f"{ns}iface.{self.name}.html" + else: + return None + + @property + def deprecated(self) -> bool: + return self.tl.INTERFACE_DEPRECATED == 1 + + +class Class(GirNode, GirType): + xml_tag = "class" + + def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: + super().__init__(ns, tl) + + @property + def abstract(self) -> bool: + return self.tl.OBJ_ABSTRACT == 1 + + @cached_property + def implements(self) -> T.List[Interface]: + n_interfaces = self.tl.OBJ_N_INTERFACES + result = [] + for i in range(n_interfaces): + entry = self.tl[self.tl.header.HEADER_OBJECT_BLOB_SIZE + i * 2].AS_DIR_ENTRY + result.append(self.get_containing(Repository)._resolve_dir_entry(entry)) + return result + + @cached_property + def own_properties(self) -> T.Mapping[str, Property]: + n_interfaces = self.tl.OBJ_N_INTERFACES + offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE + offset += (n_interfaces + n_interfaces % 2) * 2 + offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE + offset += ( + self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + ) + n_properties = self.tl.OBJ_N_PROPERTIES + property_size = self.tl.header.HEADER_PROPERTY_BLOB_SIZE + result = {} + for i in range(n_properties): + property = Property(self, self.tl[offset + i * property_size]) + result[property.name] = property + return result + + @cached_property + def own_signals(self) -> T.Mapping[str, Signal]: + n_interfaces = self.tl.OBJ_N_INTERFACES + offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE + offset += (n_interfaces + n_interfaces % 2) * 2 + offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE + offset += ( + self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE + ) + offset += self.tl.OBJ_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE + offset += self.tl.OBJ_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE + n_signals = self.tl.OBJ_N_SIGNALS + signal_size = self.tl.header.HEADER_SIGNAL_BLOB_SIZE + result = {} + for i in range(n_signals): + signal = Signal(self, self.tl[offset][i * signal_size]) + result[signal.name] = signal + return result + + @cached_property + def parent(self) -> T.Optional["Class"]: + if entry := self.tl.OBJ_PARENT: + return self.get_containing(Repository)._resolve_dir_entry(entry) + else: + return None + + @cached_property + def signature(self) -> str: + assert self.container is not None + result = f"class {self.container.name}.{self.name}" + if self.parent is not None: + assert self.parent.container is not None + result += f" : {self.parent.container.name}.{self.parent.name}" + if len(self.implements): + result += " implements " + ", ".join( + [impl.full_name for impl in self.implements] + ) + return result + + @cached_property + def properties(self) -> T.Mapping[str, Property]: + return {p.name: p for p in self._enum_properties()} + + @cached_property + def signals(self) -> T.Mapping[str, Signal]: + return {s.name: s for s in self._enum_signals()} + + def assignable_to(self, other: GirType) -> bool: + if self == other: + return True + elif self.parent and self.parent.assignable_to(other): + return True + else: + for iface in self.implements: + if iface.assignable_to(other): + return True + + return False + + def _enum_properties(self) -> T.Iterable[Property]: + yield from self.own_properties.values() + + if self.parent is not None: + yield from self.parent.properties.values() + + for impl in self.implements: + yield from impl.properties.values() + + def _enum_signals(self) -> T.Iterable[Signal]: + yield from self.own_signals.values() + + if self.parent is not None: + yield from self.parent.signals.values() + + for impl in self.implements: + yield from impl.signals.values() + + @property + def online_docs(self) -> T.Optional[str]: + if ns := self.get_containing(Namespace).online_docs: + return f"{ns}class.{self.name}.html" + else: + return None + + @property + def deprecated(self) -> bool: + return self.tl.OBJ_DEPRECATED == 1 + + +class TemplateType(GirType): + def __init__(self, name: str, parent: T.Optional[GirType]): + self._name = name + self.parent = parent + + @property + def name(self) -> str: + return self._name + + @property + def full_name(self) -> str: + return self._name + + @property + def glib_type_name(self) -> str: + return self._name + + @cached_property + def properties(self) -> T.Mapping[str, Property]: + if not (isinstance(self.parent, Class) or isinstance(self.parent, Interface)): + return {} + else: + return self.parent.properties + + @cached_property + def signals(self) -> T.Mapping[str, Signal]: + if not (isinstance(self.parent, Class) or isinstance(self.parent, Interface)): + return {} + else: + return self.parent.signals + + def assignable_to(self, other: "GirType") -> bool: + if self == other: + return True + elif isinstance(other, Interface): + # we don't know the template type's interfaces, assume yes + return True + elif self.parent is None or isinstance(self.parent, ExternType): + return isinstance(other, Class) or isinstance(other, ExternType) + else: + return self.parent.assignable_to(other) + + @cached_property + def signature(self) -> str: + if self.parent is None: + return f"template {self.name}" + else: + return f"template {self.name} : {self.parent.full_name}" + + @property + def incomplete(self) -> bool: + return True + + +class EnumMember(GirNode): + xml_tag = "member" + + def __init__(self, enum: "Enumeration", tl: typelib.Typelib) -> None: + super().__init__(enum, tl) + + @property + def value(self) -> int: + return self.tl.VALUE_VALUE + + @cached_property + def name(self) -> str: + return self.tl.VALUE_NAME + + @cached_property + def nick(self) -> str: + return self.name.replace("_", "-") + + @property + def c_ident(self) -> str: + return self.tl.attr("c:identifier") + + @property + def signature(self) -> str: + return f"enum member {self.full_name} = {self.value}" + + +class Enumeration(GirNode, GirType): + xml_tag = "enumeration" + + def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: + super().__init__(ns, tl) + + @cached_property + def members(self) -> T.Dict[str, EnumMember]: + members = {} + n_values = self.tl.ENUM_N_VALUES + values = self.tl.ENUM_VALUES + value_size = self.tl.header.HEADER_VALUE_BLOB_SIZE + for i in range(n_values): + member = EnumMember(self, values[i * value_size]) + members[member.name] = member + return members + + @property + def signature(self) -> str: + return f"enum {self.full_name}" + + def assignable_to(self, type: GirType) -> bool: + return type == self + + @property + def online_docs(self) -> T.Optional[str]: + if ns := self.get_containing(Namespace).online_docs: + return f"{ns}enum.{self.name}.html" + else: + return None + + @property + def deprecated(self) -> bool: + return self.tl.ENUM_DEPRECATED == 1 + + +class Boxed(GirNode, GirType): + xml_tag = "glib:boxed" + + def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: + super().__init__(ns, tl) + + @property + def signature(self) -> str: + return f"boxed {self.full_name}" + + def assignable_to(self, type) -> bool: + return type == self + + @property + def online_docs(self) -> T.Optional[str]: + if ns := self.get_containing(Namespace).online_docs: + return f"{ns}boxed.{self.name}.html" + else: + return None + + @property + def deprecated(self) -> bool: + return self.tl.STRUCT_DEPRECATED == 1 + + +class Bitfield(Enumeration): + xml_tag = "bitfield" + + def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None: + super().__init__(ns, tl) + + +class Namespace(GirNode): + def __init__(self, repo: "Repository", tl: typelib.Typelib) -> None: + super().__init__(repo, tl) + + @cached_property + def entries(self) -> T.Mapping[str, GirType]: + entries: dict[str, GirType] = {} + + n_local_entries: int = self.tl.HEADER_N_ENTRIES + directory: typelib.Typelib = self.tl.HEADER_DIRECTORY + blob_size: int = self.tl.header.HEADER_ENTRY_BLOB_SIZE + + for i in range(n_local_entries): + entry = directory[i * blob_size] + entry_name: str = entry.DIR_ENTRY_NAME + entry_type: int = entry.DIR_ENTRY_BLOB_TYPE + entry_blob: typelib.Typelib = entry.DIR_ENTRY_OFFSET + + if entry_type == typelib.BLOB_TYPE_ENUM: + entries[entry_name] = Enumeration(self, entry_blob) + elif entry_type == typelib.BLOB_TYPE_FLAGS: + entries[entry_name] = Bitfield(self, entry_blob) + elif entry_type == typelib.BLOB_TYPE_OBJECT: + entries[entry_name] = Class(self, entry_blob) + elif entry_type == typelib.BLOB_TYPE_INTERFACE: + entries[entry_name] = Interface(self, entry_blob) + elif ( + entry_type == typelib.BLOB_TYPE_BOXED + or entry_type == typelib.BLOB_TYPE_STRUCT + ): + entries[entry_name] = Boxed(self, entry_blob) + + return entries + + @cached_property + def xml(self): + return get_xml(self.name, self.version).get_elements("namespace")[0] + + @cached_property + def name(self) -> str: + return self.tl.HEADER_NAMESPACE + + @cached_property + def version(self) -> str: + return self.tl.HEADER_NSVERSION + + @property + def signature(self) -> str: + return f"namespace {self.name} {self.version}" + + @cached_property + def classes(self) -> T.Mapping[str, Class]: + return { + name: entry + for name, entry in self.entries.items() + if isinstance(entry, Class) + } + + @cached_property + def interfaces(self) -> T.Mapping[str, Interface]: + return { + name: entry + for name, entry in self.entries.items() + if isinstance(entry, Interface) + } + + def get_type(self, name) -> T.Optional[GirType]: + """Gets a type (class, interface, enum, etc.) from this namespace.""" + return self.entries.get(name) + + def get_type_by_cname(self, cname: str) -> T.Optional[GirType]: + """Gets a type from this namespace by its C name.""" + for basic in _BASIC_TYPES.values(): + if basic.glib_type_name == cname: + return basic() + + for item in self.entries.values(): + if ( + hasattr(item, "cname") + and item.cname is not None + and item.cname == cname + ): + return item + return None + + def lookup_type(self, type_name: str) -> T.Optional[GirType]: + """Looks up a type in the scope of this namespace (including in the + namespace's dependencies).""" + + if type_name in _BASIC_TYPES: + return _BASIC_TYPES[type_name]() + elif "." in type_name: + ns, name = type_name.split(".", 1) + return self.get_containing(Repository).get_type(name, ns) + else: + return self.get_type(type_name) + + @property + def online_docs(self) -> T.Optional[str]: + return ONLINE_DOCS.get(f"{self.name}-{self.version}") + + +class Repository(GirNode): + def __init__(self, tl: typelib.Typelib) -> None: + super().__init__(None, tl) + + self.namespace = Namespace(self, tl) + + if dependencies := tl[0x24].string: + deps = [tuple(dep.split("-", 1)) for dep in dependencies.split("|")] + try: + self.includes = { + name: get_namespace(name, version) for name, version in deps + } + except: + raise CompilerBugError(f"Failed to load dependencies.") + else: + self.includes = {} + + def get_type(self, name: str, ns: str) -> T.Optional[GirType]: + return self.lookup_namespace(ns).get_type(name) + + def get_type_by_cname(self, name: str) -> T.Optional[GirType]: + for ns in [self.namespace, *self.includes.values()]: + if type := ns.get_type_by_cname(name): + return type + return None + + def lookup_namespace(self, ns: str): + """Finds a namespace among this namespace's dependencies.""" + if ns == self.namespace.name: + return self.namespace + else: + for include in self.includes.values(): + if namespace := include.get_containing(Repository).lookup_namespace(ns): + return namespace + + def _resolve_dir_entry(self, dir_entry: typelib.Typelib): + if dir_entry.DIR_ENTRY_LOCAL: + return self.namespace.get_type(dir_entry.DIR_ENTRY_NAME) + else: + ns = dir_entry.DIR_ENTRY_NAMESPACE + return self.lookup_namespace(ns).get_type(dir_entry.DIR_ENTRY_NAME) + + def _resolve_type_id(self, type_id: int) -> GirType: + if type_id & (0xFFFFFF if sys.byteorder == "little" else 0xFFFFFF00) == 0: + type_id = ((type_id >> 27) if sys.byteorder == "little" else type_id) & 0x1F + # simple type + if type_id == typelib.TYPE_BOOLEAN: + return BoolType() + elif type_id in [typelib.TYPE_FLOAT, typelib.TYPE_DOUBLE]: + return FloatType() + elif type_id in [ + typelib.TYPE_INT8, + typelib.TYPE_INT16, + typelib.TYPE_INT32, + typelib.TYPE_INT64, + ]: + return IntType() + elif type_id in [ + typelib.TYPE_UINT8, + typelib.TYPE_UINT16, + typelib.TYPE_UINT32, + typelib.TYPE_UINT64, + ]: + return UIntType() + elif type_id == typelib.TYPE_UTF8: + return StringType() + elif type_id == typelib.TYPE_GTYPE: + return TypeType() + else: + raise CompilerBugError("Unknown type ID", type_id) + else: + blob = self.tl.header[type_id] + if blob.TYPE_BLOB_TAG == typelib.TYPE_INTERFACE: + return self._resolve_dir_entry( + self.tl.header[type_id].TYPE_BLOB_INTERFACE + ) + elif blob.TYPE_BLOB_TAG == typelib.TYPE_ARRAY: + return ArrayType(self._resolve_type_id(blob.TYPE_BLOB_ARRAY_INNER)) + else: + raise CompilerBugError(f"{blob.TYPE_BLOB_TAG}") + + +class GirContext: + def __init__(self): + self.namespaces = {} + self.not_found_namespaces: T.Set[str] = set() + + def add_namespace(self, namespace: Namespace): + other = self.namespaces.get(namespace.name) + if other is not None and other.version != namespace.version: + raise CompileError( + f"Namespace {namespace.name}-{namespace.version} can't be imported because version {other.version} was imported earlier" + ) + + self.namespaces[namespace.name] = namespace + + def get_type_by_cname(self, name: str) -> T.Optional[GirType]: + for ns in self.namespaces.values(): + if type := ns.get_type_by_cname(name): + return type + return None + + def get_type(self, name: str, ns: str) -> T.Optional[GirType]: + if ns is None and name in _BASIC_TYPES: + return _BASIC_TYPES[name]() + + ns = ns or "Gtk" + + if ns not in self.namespaces: + return None + + return self.namespaces[ns].get_type(name) + + def get_class(self, name: str, ns: str) -> T.Optional[Class]: + type = self.get_type(name, ns) + if isinstance(type, Class): + return type + else: + return None + + def validate_ns(self, ns: str) -> None: + """Raises an exception if there is a problem looking up the given + namespace.""" + + ns = ns or "Gtk" + + if ns not in self.namespaces and ns not in self.not_found_namespaces: + all_available = list(set(ns for ns, _version in get_available_namespaces())) + + raise CompileError( + f"Namespace {ns} was not imported", + did_you_mean=(ns, all_available), + ) + + def validate_type(self, name: str, ns: str) -> None: + """Raises an exception if there is a problem looking up the given type.""" + + self.validate_ns(ns) + + type = self.get_type(name, ns) + + ns = ns or "Gtk" + + if type is None: + raise CompileError( + f"Namespace {ns} does not contain a type called {name}", + did_you_mean=(name, self.namespaces[ns].classes.keys()), + ) diff --git a/gtk/blueprint-compiler/blueprintcompiler/interactive_port.py b/gtk/blueprint-compiler/blueprintcompiler/interactive_port.py new file mode 100644 index 00000000000..0c37885eb0d --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/interactive_port.py @@ -0,0 +1,341 @@ +# interactive_port.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +import difflib +import os +import typing as T + +from . import decompiler, parser, tokenizer +from .errors import CompilerBugError, MultipleErrors, PrintableError +from .outputs.xml import XmlOutput +from .utils import Colors + +# A tool to interactively port projects to blueprints. + + +class CouldNotPort: + def __init__(self, message: str): + self.message = message + + +def change_suffix(f): + return f.removesuffix(".ui") + ".blp" + + +def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]: + if os.path.exists(out_file): + return CouldNotPort("already exists") + + try: + decompiled = decompiler.decompile(in_file) + + try: + # make sure the output compiles + tokens = tokenizer.tokenize(decompiled) + ast, errors, warnings = parser.parse(tokens) + + for warning in warnings: + warning.pretty_print(out_file, decompiled) + + if errors: + raise errors + if not ast: + raise CompilerBugError() + + output = XmlOutput() + output.emit(ast) + except PrintableError as e: + e.pretty_print(out_file, decompiled) + + print( + f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}" + ) + print(f"in {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE}") + print( + f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the +porting tool. If you think it's a bug (which is likely), please file an issue on GitLab: +{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""" + ) + + return CouldNotPort("does not compile") + + return decompiled + + except decompiler.UnsupportedError as e: + e.print(in_file) + return CouldNotPort("could not convert") + + +def listdir_recursive(subdir): + files = os.listdir(subdir) + for file in files: + if file in ["_build", "build"]: + continue + if file.startswith("."): + continue + full = os.path.join(subdir, file) + if full == "./subprojects": + # skip the subprojects directory + continue + if os.path.isfile(full): + yield full + elif os.path.isdir(full): + yield from listdir_recursive(full) + + +def yesno(prompt): + while True: + response = input(f"{Colors.BOLD}{prompt} [y/n] {Colors.CLEAR}") + if response.lower() in ["yes", "y"]: + return True + elif response.lower() in ["no", "n"]: + return False + + +def enter(): + input(f"{Colors.BOLD}Press Enter when you have done that: {Colors.CLEAR}") + + +def step1(): + print( + f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}" + ) + + if os.path.exists("subprojects/blueprint-compiler.wrap"): + print("subprojects/blueprint-compiler.wrap already exists, skipping\n") + return + + if yesno("Create subprojects/blueprint-compiler.wrap?"): + try: + os.mkdir("subprojects") + except: + pass + + from .main import VERSION + + VERSION = "main" if VERSION == "uninstalled" else "v" + VERSION + + with open("subprojects/blueprint-compiler.wrap", "w") as wrap: + wrap.write( + f"""[wrap-git] +directory = blueprint-compiler +url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git +revision = {VERSION} +depth = 1 + +[provide] +program_names = blueprint-compiler""" + ) + + print() + + +def step2(): + print(f"{Colors.BOLD}STEP 2: Set up .gitignore{Colors.CLEAR}") + + if os.path.exists(".gitignore"): + with open(".gitignore", "r+") as gitignore: + ignored = [line.strip() for line in gitignore.readlines()] + if "/subprojects/blueprint-compiler" not in ignored: + if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"): + gitignore.write("\n/subprojects/blueprint-compiler\n") + else: + print( + "'/subprojects/blueprint-compiler' already in .gitignore, skipping" + ) + else: + if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"): + with open(".gitignore", "w") as gitignore: + gitignore.write("/subprojects/blueprint-compiler\n") + + print() + + +def step3(): + print(f"{Colors.BOLD}STEP 3: Convert UI files{Colors.CLEAR}") + + files = [ + (file, change_suffix(file), decompile_file(file, change_suffix(file))) + for file in listdir_recursive(".") + if file.endswith(".ui") + ] + + success = 0 + for in_file, out_file, result in files: + if isinstance(result, CouldNotPort): + if result.message == "already exists": + print(Colors.FAINT, end="") + print( + f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}" + ) + else: + print( + f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}" + ) + success += 1 + + print() + if len(files) == 0: + print(f"{Colors.RED}No UI files found.{Colors.CLEAR}") + elif success == len(files): + print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}") + elif success > 0: + print( + f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}" + ) + else: + print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}") + + if success > 0 and yesno("Save these changes?"): + for in_file, out_file, result in files: + if not isinstance(result, CouldNotPort): + with open(out_file, "x") as file: + file.write(result) + + print() + results = [ + (in_file, out_file) + for in_file, out_file, result in files + if not isinstance(result, CouldNotPort) or result.message == "already exists" + ] + if len(results): + return zip(*results) + else: + return ([], []) + + +def step4(ported): + print(f"{Colors.BOLD}STEP 4: Set up meson.build{Colors.CLEAR}") + print( + f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}" + ) + + meson_files = [ + file + for file in listdir_recursive(".") + if os.path.basename(file) == "meson.build" + ] + for meson_file in meson_files: + with open(meson_file, "r") as f: + if "gnome.compile_resources" in f.read(): + parent = os.path.dirname(meson_file) + file_list = "\n ".join( + [ + f"'{os.path.relpath(file, parent)}'," + for file in ported + if file.startswith(parent) + ] + ) + + if len(file_list): + print( + f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}" + ) + print( + f""" +blueprints = custom_target('blueprints', + input: files( + {file_list} + ), + output: '.', + command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], +) +""" + ) + enter() + + print( + f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()' +arguments in {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR} + +dependencies: blueprints, + """ + ) + enter() + + print() + + +def step5(in_files): + print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}") + + if not os.path.exists("po/POTFILES.in"): + print( + f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n" + ) + return + + with open("po/POTFILES.in", "r") as potfiles: + old_lines = potfiles.readlines() + lines = old_lines.copy() + for in_file in in_files: + for i, line in enumerate(lines): + if line.strip() == in_file.removeprefix("./"): + lines[i] = change_suffix(line.strip()) + "\n" + + new_data = "".join(lines) + + print( + f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}" + ) + print( + "".join( + [ + ( + Colors.GREEN + if line.startswith("+") + else Colors.RED + Colors.FAINT if line.startswith("-") else "" + ) + + line + + Colors.CLEAR + for line in difflib.unified_diff(old_lines, lines) + ] + ) + ) + + if yesno("Is this ok?"): + with open("po/POTFILES.in", "w") as potfiles: + potfiles.writelines(lines) + + print() + + +def step6(in_files): + print(f"{Colors.BOLD}STEP 6: Clean up{Colors.CLEAR}") + + if yesno("Delete old XML files?"): + for file in in_files: + try: + os.remove(file) + except: + pass + + +def run(opts): + step1() + step2() + in_files, out_files = step3() + step4(out_files) + step5(in_files) + step6(in_files) + + print( + f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}" + ) diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/__init__.py b/gtk/blueprint-compiler/blueprintcompiler/language/__init__.py new file mode 100644 index 00000000000..b30268682a9 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/__init__.py @@ -0,0 +1,78 @@ +from .adw_breakpoint import ( + AdwBreakpointCondition, + AdwBreakpointSetter, + AdwBreakpointSetters, +) +from .adw_response_dialog import ExtAdwResponseDialog +from .attributes import BaseAttribute +from .binding import Binding +from .common import * +from .contexts import ScopeCtx, ValueTypeCtx +from .expression import ( + CastExpr, + ClosureArg, + ClosureExpr, + ExprBase, + Expression, + LiteralExpr, + LookupOp, +) +from .gobject_object import Object, ObjectContent +from .gobject_property import Property +from .gobject_signal import Signal +from .gtk_a11y import ExtAccessibility +from .gtk_combo_box_text import ExtComboBoxItems +from .gtk_file_filter import ( + Filters, + ext_file_filter_mime_types, + ext_file_filter_patterns, + ext_file_filter_suffixes, +) +from .gtk_layout import ExtLayout +from .gtk_list_item_factory import ExtListItemFactory +from .gtk_menu import Menu, MenuAttribute, menu +from .gtk_scale import ExtScaleMarks +from .gtk_size_group import ExtSizeGroupWidgets +from .gtk_string_list import ExtStringListStrings +from .gtk_styles import ExtStyles +from .gtkbuilder_child import Child, ChildExtension, ChildInternal, ChildType +from .gtkbuilder_template import Template +from .imports import GtkDirective, Import +from .types import ClassName +from .ui import UI +from .values import ( + ArrayValue, + Flag, + Flags, + IdentLiteral, + Literal, + NumberLiteral, + ObjectValue, + QuotedLiteral, + StringValue, + Translated, + TypeLiteral, + Value, +) + +OBJECT_CONTENT_HOOKS.children = [ + Signal, + Property, + AdwBreakpointCondition, + AdwBreakpointSetters, + ExtAccessibility, + ExtAdwResponseDialog, + ExtComboBoxItems, + ext_file_filter_mime_types, + ext_file_filter_patterns, + ext_file_filter_suffixes, + ExtLayout, + ExtListItemFactory, + ExtScaleMarks, + ExtSizeGroupWidgets, + ExtStringListStrings, + ExtStyles, + Child, +] + +LITERAL.children = [Literal] diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/adw_breakpoint.py b/gtk/blueprint-compiler/blueprintcompiler/language/adw_breakpoint.py new file mode 100644 index 00000000000..4ad5b24f608 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/adw_breakpoint.py @@ -0,0 +1,251 @@ +# adw_breakpoint.py +# +# Copyright 2023 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +from .common import * +from .contexts import ScopeCtx, ValueTypeCtx +from .gobject_object import Object, validate_parent_type +from .values import Value + + +class AdwBreakpointCondition(AstNode): + grammar = [ + UseExact("kw", "condition"), + "(", + UseQuoted("condition"), + Match(")").expected(), + ] + + @property + def condition(self) -> str: + return self.tokens["condition"] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "condition", + SymbolKind.Property, + self.range, + self.group.tokens["kw"].range, + self.condition, + ) + + @docs("kw") + def keyword_docs(self): + klass = self.root.gir.get_type("Breakpoint", "Adw") + if klass is None: + return None + prop = klass.properties.get("condition") + assert isinstance(prop, gir.Property) + return prop.doc + + @validate() + def unique(self): + self.validate_unique_in_parent("Duplicate condition statement") + + +class AdwBreakpointSetter(AstNode): + grammar = Statement( + UseIdent("object"), + Match(".").expected(), + UseIdent("property"), + Match(":").expected(), + Value, + ) + + @property + def object_id(self) -> str: + return self.tokens["object"] + + @property + def object(self) -> T.Optional[Object]: + return self.context[ScopeCtx].objects.get(self.object_id) + + @property + def property_name(self) -> T.Optional[str]: + return self.tokens["property"] + + @property + def value(self) -> Value: + return self.children[Value][0] + + @property + def gir_class(self) -> T.Optional[GirType]: + if self.object is not None: + return self.object.gir_class + else: + return None + + @property + def gir_property(self) -> T.Optional[gir.Property]: + if ( + self.gir_class is not None + and not isinstance(self.gir_class, ExternType) + and self.property_name is not None + ): + assert isinstance(self.gir_class, gir.Class) or isinstance( + self.gir_class, gir.TemplateType + ) + return self.gir_class.properties.get(self.property_name) + else: + return None + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + f"{self.object_id}.{self.property_name}", + SymbolKind.Property, + self.range, + self.group.tokens["object"].range, + self.value.range.text, + ) + + def get_reference(self, idx: int) -> T.Optional[LocationLink]: + if idx in self.group.tokens["object"].range: + if self.object is not None: + return LocationLink( + self.group.tokens["object"].range, + self.object.range, + self.object.ranges["id"], + ) + + return None + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + if self.gir_property is not None: + type = self.gir_property.type + else: + type = None + + return ValueTypeCtx(type, allow_null=True) + + @docs("object") + def object_docs(self): + if self.object is not None: + return f"```\n{self.object.signature}\n```" + else: + return None + + @docs("property") + def property_docs(self): + if self.gir_property is not None: + return self.gir_property.doc + else: + return None + + @validate("object") + def object_exists(self): + if self.object is None: + raise CompileError( + f"Could not find object with ID {self.object_id}", + did_you_mean=(self.object_id, self.context[ScopeCtx].objects.keys()), + ) + + @validate("property") + def property_exists(self): + if self.gir_class is None or self.gir_class.incomplete: + # Objects that we have no gir data on should not be validated + # This happens for classes defined by the app itself + return + + if self.gir_property is None and self.property_name is not None: + raise CompileError( + f"Class {self.gir_class.full_name} does not have a property called {self.property_name}", + did_you_mean=(self.property_name, self.gir_class.properties.keys()), + ) + + @validate() + def unique(self): + self.validate_unique_in_parent( + f"Duplicate setter for {self.object_id}.{self.property_name}", + lambda x: x.object_id == self.object_id + and x.property_name == self.property_name, + ) + + +class AdwBreakpointSetters(AstNode): + grammar = [ + Keyword("setters"), + Match("{").expected(), + Until(AdwBreakpointSetter, "}"), + ] + + @property + def setters(self) -> T.List[AdwBreakpointSetter]: + return self.children[AdwBreakpointSetter] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "setters", + SymbolKind.Struct, + self.range, + self.group.tokens["setters"].range, + ) + + @validate() + def container_is_breakpoint(self): + validate_parent_type(self, "Adw", "Breakpoint", "breakpoint setters") + + @validate() + def unique(self): + self.validate_unique_in_parent("Duplicate setters block") + + @docs("setters") + def ref_docs(self): + return get_docs_section("Syntax ExtAdwBreakpoint") + + +@decompiler("condition", cdata=True) +def decompile_condition(ctx: DecompileCtx, gir, cdata): + ctx.print(f"condition({escape_quote(cdata)})") + + +@decompiler("setter", element=True) +def decompile_setter(ctx: DecompileCtx, gir, element): + assert ctx.parent_node is not None + # only run for the first setter + for child in ctx.parent_node.children: + if child.tag == "setter": + if child != element: + # already decompiled + return + else: + break + + ctx.print("setters {") + for child in ctx.parent_node.children: + if child.tag == "setter": + object_id = child["object"] + property_name = child["property"] + obj = ctx.find_object(object_id) + if obj is not None: + gir_class = ctx.type_by_cname(obj["class"]) + else: + gir_class = None + + if object_id == ctx.template_class: + object_id = "template" + + comments, string = ctx.decompile_value( + child.cdata, + gir_class, + (child["translatable"], child["context"], child["comments"]), + ) + ctx.print(f"{comments} {object_id}.{property_name}: {string};") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/adw_response_dialog.py b/gtk/blueprint-compiler/blueprintcompiler/language/adw_response_dialog.py new file mode 100644 index 00000000000..5493d4d803b --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/adw_response_dialog.py @@ -0,0 +1,197 @@ +# adw_response_dialog.py +# +# Copyright 2023 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from ..decompiler import decompile_translatable, truthy +from .common import * +from .contexts import ValueTypeCtx +from .gobject_object import ObjectContent, validate_parent_type +from .values import StringValue + + +class ExtAdwResponseDialogFlag(AstNode): + grammar = AnyOf( + UseExact("flag", "destructive"), + UseExact("flag", "suggested"), + UseExact("flag", "disabled"), + ) + + @property + def flag(self) -> str: + return self.tokens["flag"] + + @validate() + def unique(self): + self.validate_unique_in_parent( + f"Duplicate '{self.flag}' flag", check=lambda child: child.flag == self.flag + ) + + @validate() + def exclusive(self): + if self.flag in ["destructive", "suggested"]: + self.validate_unique_in_parent( + "'suggested' and 'destructive' are exclusive", + check=lambda child: child.flag in ["destructive", "suggested"], + ) + + +class ExtAdwResponseDialogResponse(AstNode): + grammar = [ + UseIdent("id"), + Match(":").expected(), + to_parse_node(StringValue).expected("a string or translatable string"), + ZeroOrMore(ExtAdwResponseDialogFlag), + ] + + @property + def id(self) -> str: + return self.tokens["id"] + + @property + def flags(self) -> T.List[ExtAdwResponseDialogFlag]: + return self.children[ExtAdwResponseDialogFlag] + + @property + def appearance(self) -> T.Optional[str]: + if any(flag.flag == "destructive" for flag in self.flags): + return "destructive" + elif any(flag.flag == "suggested" for flag in self.flags): + return "suggested" + else: + return None + + @property + def enabled(self) -> bool: + return not any(flag.flag == "disabled" for flag in self.flags) + + @property + def value(self) -> StringValue: + return self.children[0] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.id, + SymbolKind.Field, + self.range, + self.group.tokens["id"].range, + self.value.range.text, + ) + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(StringType()) + + @validate("id") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate response ID '{self.id}'", + check=lambda child: child.id == self.id, + ) + + +class ExtAdwResponseDialog(AstNode): + grammar = [ + Keyword("responses"), + Match("[").expected(), + Delimited(ExtAdwResponseDialogResponse, ","), + "]", + ] + + @property + def responses(self) -> T.List[ExtAdwResponseDialogResponse]: + return self.children + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "responses", + SymbolKind.Array, + self.range, + self.group.tokens["responses"].range, + ) + + @validate("responses") + def container_is_message_dialog_or_alert_dialog(self): + try: + validate_parent_type(self, "Adw", "MessageDialog", "responses") + except: + validate_parent_type(self, "Adw", "AlertDialog", "responses") + + @validate("responses") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate responses block") + + @docs() + def ref_docs(self): + return get_docs_section("Syntax ExtAdwMessageDialog") + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Adw", "MessageDialog"), + matches=new_statement_patterns, +) +def complete_adw_message_dialog(lsp, ast_node, match_variables): + yield Completion( + "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" + ) + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Adw", "AlertDialog"), + matches=new_statement_patterns, +) +def complete_adw_alert_dialog(lsp, ast_node, match_variables): + yield Completion( + "responses", CompletionItemKind.Keyword, snippet="responses [\n\t$0\n]" + ) + + +@decompiler("responses") +def decompile_responses(ctx, gir): + ctx.print(f"responses [") + + +@decompiler("response", cdata=True) +def decompile_response( + ctx, + gir, + cdata, + id, + appearance=None, + enabled=None, + translatable=None, + context=None, + comments=None, +): + comments, translated = decompile_translatable( + cdata, translatable, context, comments + ) + if comments is not None: + ctx.print(comments) + + flags = "" + if appearance is not None: + flags += f" {appearance}" + if enabled is not None and not truthy(enabled): + flags += " disabled" + + ctx.print(f"{id}: {translated}{flags},") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/attributes.py b/gtk/blueprint-compiler/blueprintcompiler/language/attributes.py new file mode 100644 index 00000000000..8ff1f0b8d52 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/attributes.py @@ -0,0 +1,32 @@ +# attributes.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .common import * + + +class BaseAttribute(AstNode): + """A helper class for attribute syntax of the form `name: literal_value;`""" + + tag_name: str = "" + attr_name: str = "name" + + @property + def name(self): + return self.tokens["name"] diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/binding.py b/gtk/blueprint-compiler/blueprintcompiler/language/binding.py new file mode 100644 index 00000000000..07572a9e2fb --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/binding.py @@ -0,0 +1,123 @@ +# binding.py +# +# Copyright 2023 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +from dataclasses import dataclass + +from .common import * +from .expression import Expression, LiteralExpr, LookupOp + + +class BindingFlag(AstNode): + grammar = [ + AnyOf( + UseExact("flag", "inverted"), + UseExact("flag", "bidirectional"), + UseExact("flag", "no-sync-create"), + UseExact("flag", "sync-create"), + ) + ] + + @property + def flag(self) -> str: + return self.tokens["flag"] + + @validate() + def sync_create(self): + if self.flag == "sync-create": + raise UpgradeWarning( + "'sync-create' is now the default. Use 'no-sync-create' if this is not wanted.", + actions=[CodeAction("remove 'sync-create'", "")], + ) + + @validate() + def unique(self): + self.validate_unique_in_parent( + f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag + ) + + @validate() + def flags_only_if_simple(self): + if self.parent.simple_binding is None: + raise CompileError( + "Only bindings with a single lookup can have flags", + ) + + @docs() + def ref_docs(self): + return get_docs_section("Syntax Binding") + + +class Binding(AstNode): + grammar = [ + AnyOf(Keyword("bind"), UseExact("bind", "bind-property")), + Expression, + ZeroOrMore(BindingFlag), + ] + + @property + def expression(self) -> Expression: + return self.children[Expression][0] + + @property + def flags(self) -> T.List[BindingFlag]: + return self.children[BindingFlag] + + @property + def simple_binding(self) -> T.Optional["SimpleBinding"]: + if isinstance(self.expression.last, LookupOp): + if isinstance(self.expression.last.lhs, LiteralExpr): + from .values import IdentLiteral + + if isinstance(self.expression.last.lhs.literal.value, IdentLiteral): + flags = [x.flag for x in self.flags] + return SimpleBinding( + self.expression.last.lhs.literal.value.ident, + self.expression.last.property_name, + no_sync_create="no-sync-create" in flags, + bidirectional="bidirectional" in flags, + inverted="inverted" in flags, + ) + return None + + @validate("bind") + def bind_property(self): + if self.tokens["bind"] == "bind-property": + raise UpgradeWarning( + "'bind-property' is no longer needed. Use 'bind' instead. (blueprint 0.8.2)", + actions=[CodeAction("use 'bind'", "bind")], + ) + + @docs("bind") + def ref_docs(self): + return get_docs_section("Syntax Binding") + + +@dataclass +class SimpleBinding: + source: str + property_name: str + no_sync_create: bool = False + bidirectional: bool = False + inverted: bool = False + + +@decompiler("binding") +def decompile_binding(ctx: DecompileCtx, gir: gir.GirContext, name: str): + ctx.end_block_with(";") + ctx.print(f"{name}: bind ") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/common.py b/gtk/blueprint-compiler/blueprintcompiler/language/common.py new file mode 100644 index 00000000000..1cc1b3b6dd2 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/common.py @@ -0,0 +1,63 @@ +# common.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .. import decompiler as decompile +from .. import gir +from ..ast_utils import AstNode, context, docs, validate +from ..completions_utils import * +from ..decompiler import ( + DecompileCtx, + decompile_translatable, + decompiler, + escape_quote, + truthy, +) +from ..errors import ( + CodeAction, + CompileError, + CompileWarning, + DeprecatedWarning, + MultipleErrors, + UnusedWarning, + UpgradeWarning, +) +from ..gir import ( + BoolType, + Enumeration, + ExternType, + FloatType, + GirType, + IntType, + StringType, +) +from ..lsp_utils import ( + Completion, + CompletionItemKind, + DocumentSymbol, + LocationLink, + SemanticToken, + SemanticTokenType, + SymbolKind, + get_docs_section, +) +from ..parse_tree import * + +OBJECT_CONTENT_HOOKS = AnyOf() +LITERAL = AnyOf() diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/contexts.py b/gtk/blueprint-compiler/blueprintcompiler/language/contexts.py new file mode 100644 index 00000000000..c5e97b3f4b6 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/contexts.py @@ -0,0 +1,81 @@ +# contexts.py +# +# Copyright 2023 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import typing as T +from dataclasses import dataclass +from functools import cached_property + +from .common import * +from .gobject_object import Object +from .gtkbuilder_template import Template + + +@dataclass +class ValueTypeCtx: + value_type: T.Optional[GirType] + allow_null: bool = False + must_infer_type: bool = False + + +@dataclass +class ScopeCtx: + node: AstNode + + @cached_property + def template(self): + from .gtk_list_item_factory import ExtListItemFactory + from .ui import UI + + if isinstance(self.node, UI): + return self.node.template + elif isinstance(self.node, ExtListItemFactory): + return self.node + + @cached_property + def objects(self) -> T.Dict[str, Object]: + return { + obj.tokens["id"]: obj + for obj in self._iter_recursive(self.node) + if obj.tokens["id"] is not None + } + + def validate_unique_ids(self) -> None: + from .gtk_list_item_factory import ExtListItemFactory + + passed = {} + for obj in self._iter_recursive(self.node): + if obj.tokens["id"] is None: + continue + + if obj.tokens["id"] in passed: + token = obj.group.tokens["id"] + if not isinstance(obj, Template) and not isinstance( + obj, ExtListItemFactory + ): + raise CompileError( + f"Duplicate object ID '{obj.tokens['id']}'", + token.range, + ) + passed[obj.tokens["id"]] = obj + + def _iter_recursive(self, node: AstNode): + yield node + for child in node.children: + if child.context[ScopeCtx] is self: + yield from self._iter_recursive(child) diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/expression.py b/gtk/blueprint-compiler/blueprintcompiler/language/expression.py new file mode 100644 index 00000000000..ae9c3994e8e --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/expression.py @@ -0,0 +1,369 @@ +# expressions.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from ..decompiler import decompile_element +from .common import * +from .contexts import ScopeCtx, ValueTypeCtx +from .types import TypeName + +expr = Sequence() + + +class ExprBase(AstNode): + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + if rhs := self.rhs: + return rhs.context[ValueTypeCtx] + else: + return self.parent.context[ValueTypeCtx] + + @property + def type(self) -> T.Optional[GirType]: + raise NotImplementedError() + + @property + def type_complete(self) -> bool: + return True + + @property + def rhs(self) -> T.Optional["ExprBase"]: + if isinstance(self.parent, Expression): + children = list(self.parent.children) + if children.index(self) + 1 < len(children): + return children[children.index(self) + 1] + else: + return self.parent.rhs + else: + return None + + +class Expression(ExprBase): + grammar = expr + + @property + def last(self) -> ExprBase: + return self.children[-1] + + @property + def type(self) -> T.Optional[GirType]: + return self.last.type + + @property + def type_complete(self) -> bool: + return self.last.type_complete + + +class InfixExpr(ExprBase): + @property + def lhs(self): + children = list(self.parent_by_type(Expression).children) + return children[children.index(self) - 1] + + +class LiteralExpr(ExprBase): + grammar = LITERAL + + @property + def is_object(self) -> bool: + from .values import IdentLiteral + + return isinstance(self.literal.value, IdentLiteral) and ( + self.literal.value.ident in self.context[ScopeCtx].objects + or self.root.is_legacy_template(self.literal.value.ident) + ) + + @property + def literal(self): + from .values import Literal + + return self.children[Literal][0] + + @property + def type(self) -> T.Optional[GirType]: + return self.literal.value.type + + @property + def type_complete(self) -> bool: + from .values import IdentLiteral + + if isinstance(self.literal.value, IdentLiteral): + if object := self.context[ScopeCtx].objects.get(self.literal.value.ident): + return not object.gir_class.incomplete + return True + + +class LookupOp(InfixExpr): + grammar = [".", UseIdent("property")] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(None, must_infer_type=True) + + @property + def property_name(self) -> str: + return self.tokens["property"] + + @property + def type(self) -> T.Optional[GirType]: + if isinstance(self.lhs.type, gir.Class) or isinstance( + self.lhs.type, gir.Interface + ): + if property := self.lhs.type.properties.get(self.property_name): + return property.type + + return None + + @docs("property") + def property_docs(self): + if not ( + isinstance(self.lhs.type, gir.Class) + or isinstance(self.lhs.type, gir.Interface) + ): + return None + + if property := self.lhs.type.properties.get(self.property_name): + return property.doc + + @validate("property") + def property_exists(self): + if self.lhs.type is None: + # Literal values throw their own errors if the type isn't known + if isinstance(self.lhs, LiteralExpr): + return + + raise CompileError( + f"Could not determine the type of the preceding expression", + hints=[ + f"add a type cast so blueprint knows which type the property {self.property_name} belongs to" + ], + ) + + if self.lhs.type.incomplete: + return + + elif not isinstance(self.lhs.type, gir.Class) and not isinstance( + self.lhs.type, gir.Interface + ): + raise CompileError( + f"Type {self.lhs.type.full_name} does not have properties" + ) + + elif self.lhs.type.properties.get(self.property_name) is None: + raise CompileError( + f"{self.lhs.type.full_name} does not have a property called {self.property_name}", + did_you_mean=(self.property_name, self.lhs.type.properties.keys()), + ) + + @validate("property") + def property_deprecated(self): + if self.lhs.type is None or not ( + isinstance(self.lhs.type, gir.Class) + or isinstance(self.lhs.type, gir.Interface) + ): + return + + if property := self.lhs.type.properties.get(self.property_name): + if property.deprecated: + hints = [] + if property.deprecated_doc: + hints.append(property.deprecated_doc) + raise DeprecatedWarning( + f"{property.signature} is deprecated", + hints=hints, + ) + + +class CastExpr(InfixExpr): + grammar = [ + Keyword("as"), + AnyOf( + ["<", TypeName, Match(">").expected()], + [ + UseExact("lparen", "("), + TypeName, + UseExact("rparen", ")").expected("')'"), + ], + ), + ] + + @context(ValueTypeCtx) + def value_type(self): + return ValueTypeCtx(self.type) + + @property + def type(self) -> T.Optional[GirType]: + return self.children[TypeName][0].gir_type + + @property + def type_complete(self) -> bool: + return True + + @validate() + def cast_makes_sense(self): + if self.type is None or self.lhs.type is None: + return + + if not self.type.assignable_to(self.lhs.type): + raise CompileError( + f"Invalid cast. No instance of {self.lhs.type.full_name} can be an instance of {self.type.full_name}." + ) + + @validate("lparen", "rparen") + def upgrade_to_angle_brackets(self): + if self.tokens["lparen"]: + raise UpgradeWarning( + "Use angle bracket syntax introduced in blueprint 0.8.0", + actions=[ + CodeAction( + "Use <> instead of ()", + f"<{self.children[TypeName][0].as_string}>", + ) + ], + ) + + @docs("as") + def ref_docs(self): + return get_docs_section("Syntax CastExpression") + + +class ClosureArg(AstNode): + grammar = Expression + + @property + def expr(self) -> Expression: + return self.children[Expression][0] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(None) + + +class ClosureExpr(ExprBase): + grammar = [ + Optional(["$", UseLiteral("extern", True)]), + UseIdent("name"), + "(", + Delimited(ClosureArg, ","), + ")", + ] + + @property + def type(self) -> T.Optional[GirType]: + if isinstance(self.rhs, CastExpr): + return self.rhs.type + else: + return None + + @property + def closure_name(self) -> str: + return self.tokens["name"] + + @property + def args(self) -> T.List[ClosureArg]: + return self.children[ClosureArg] + + @validate() + def cast_to_return_type(self): + if not isinstance(self.rhs, CastExpr): + raise CompileError( + "Closure expression must be cast to the closure's return type" + ) + + @validate() + def builtin_exists(self): + if not self.tokens["extern"]: + raise CompileError(f"{self.closure_name} is not a builtin function") + + @docs("name") + def ref_docs(self): + return get_docs_section("Syntax ClosureExpression") + + +expr.children = [ + AnyOf(ClosureExpr, LiteralExpr, ["(", Expression, ")"]), + ZeroOrMore(AnyOf(LookupOp, CastExpr)), +] + + +@decompiler("lookup", skip_children=True, cdata=True) +def decompile_lookup( + ctx: DecompileCtx, gir: gir.GirContext, cdata: str, name: str, type: str +): + if t := ctx.type_by_cname(type): + type = decompile.full_name(t) + else: + type = "$" + type + + assert ctx.current_node is not None + + constant = None + if len(ctx.current_node.children) == 0: + constant = cdata + elif ( + len(ctx.current_node.children) == 1 + and ctx.current_node.children[0].tag == "constant" + ): + constant = ctx.current_node.children[0].cdata + + if constant is not None: + if constant == ctx.template_class: + ctx.print("template." + name) + else: + ctx.print(constant + "." + name) + return + else: + for child in ctx.current_node.children: + decompile.decompile_element(ctx, gir, child) + + ctx.print(f" as <{type}>.{name}") + + +@decompiler("constant", cdata=True) +def decompile_constant( + ctx: DecompileCtx, gir: gir.GirContext, cdata: str, type: T.Optional[str] = None +): + if type is None: + if cdata == ctx.template_class: + ctx.print("template") + else: + ctx.print(cdata) + else: + _, string = ctx.decompile_value(cdata, ctx.type_by_cname(type)) + ctx.print(string) + + +@decompiler("closure", skip_children=True) +def decompile_closure(ctx: DecompileCtx, gir: gir.GirContext, function: str, type: str): + if t := ctx.type_by_cname(type): + type = decompile.full_name(t) + else: + type = "$" + type + + ctx.print(f"${function}(") + + assert ctx.current_node is not None + for i, node in enumerate(ctx.current_node.children): + decompile_element(ctx, gir, node) + + assert ctx.current_node is not None + if i < len(ctx.current_node.children) - 1: + ctx.print(", ") + + ctx.end_block_with(f") as <{type}>") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gobject_object.py b/gtk/blueprint-compiler/blueprintcompiler/language/gobject_object.py new file mode 100644 index 00000000000..54cb2975936 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gobject_object.py @@ -0,0 +1,128 @@ +# gobject_object.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +import typing as T +from functools import cached_property + +from blueprintcompiler.errors import T +from blueprintcompiler.lsp_utils import DocumentSymbol + +from .common import * +from .response_id import ExtResponse +from .types import ClassName, ConcreteClassName + +RESERVED_IDS = {"this", "self", "template", "true", "false", "null", "none"} + + +class ObjectContent(AstNode): + grammar = ["{", Until(OBJECT_CONTENT_HOOKS, "}")] + + @property + def gir_class(self): + return self.parent.gir_class + + +class Object(AstNode): + grammar: T.Any = [ + ConcreteClassName, + Optional(UseIdent("id")), + ObjectContent, + ] + + @property + def id(self) -> str: + return self.tokens["id"] + + @property + def class_name(self) -> ClassName: + return self.children[ClassName][0] + + @property + def content(self) -> ObjectContent: + return self.children[ObjectContent][0] + + @property + def signature(self) -> str: + if self.id: + return f"{self.class_name.gir_type.full_name} {self.id}" + elif t := self.class_name.gir_type: + return f"{t.full_name}" + else: + return f"{self.class_name.as_string}" + + @property + def document_symbol(self) -> T.Optional[DocumentSymbol]: + return DocumentSymbol( + self.class_name.as_string, + SymbolKind.Object, + self.range, + self.children[ClassName][0].range, + self.id, + ) + + @property + def gir_class(self) -> GirType: + if self.class_name is None: + raise CompilerBugError() + return self.class_name.gir_type + + @cached_property + def action_widgets(self) -> T.List[ExtResponse]: + """Get list of widget's action widgets. + + Empty if object doesn't have action widgets. + """ + from .gtkbuilder_child import Child + + return [ + child.response_id + for child in self.content.children[Child] + if child.response_id + ] + + @validate("id") + def object_id_not_reserved(self): + from .gtkbuilder_template import Template + + if not isinstance(self, Template) and self.id in RESERVED_IDS: + raise CompileWarning(f"{self.id} may be a confusing object ID") + + +def validate_parent_type(node, ns: str, name: str, err_msg: str): + parent = node.root.gir.get_type(name, ns) + container_type = node.parent_by_type(Object).gir_class + if container_type and not container_type.assignable_to(parent): + raise CompileError( + f"{container_type.full_name} is not a {ns}.{name}, so it doesn't have {err_msg}" + ) + + +@decompiler("object") +def decompile_object(ctx: DecompileCtx, gir, klass, id=None): + gir_class = ctx.type_by_cname(klass) + klass_name = ( + decompile.full_name(gir_class) if gir_class is not None else "$" + klass + ) + if id is None: + ctx.print(f"{klass_name} {{") + else: + ctx.print(f"{klass_name} {id} {{") + ctx.push_obj_type(gir_class) + return gir_class diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gobject_property.py b/gtk/blueprint-compiler/blueprintcompiler/language/gobject_property.py new file mode 100644 index 00000000000..5d0c867d4ec --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gobject_property.py @@ -0,0 +1,127 @@ +# gobject_property.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .binding import Binding +from .common import * +from .contexts import ValueTypeCtx +from .gtkbuilder_template import Template +from .values import ArrayValue, ObjectValue, Value + + +class Property(AstNode): + grammar = Statement( + UseIdent("name"), ":", AnyOf(Binding, ObjectValue, Value, ArrayValue) + ) + + @property + def name(self) -> str: + return self.tokens["name"] + + @property + def value(self) -> T.Union[Binding, ObjectValue, Value, ArrayValue]: + return self.children[0] + + @property + def gir_class(self): + return self.parent.parent.gir_class + + @property + def gir_property(self) -> T.Optional[gir.Property]: + if self.gir_class is not None and not isinstance(self.gir_class, ExternType): + return self.gir_class.properties.get(self.tokens["name"]) + else: + return None + + @property + def document_symbol(self) -> DocumentSymbol: + if isinstance(self.value, ObjectValue): + detail = None + else: + detail = self.value.range.text + + return DocumentSymbol( + self.name, + SymbolKind.Property, + self.range, + self.group.tokens["name"].range, + detail, + ) + + @validate() + def binding_valid(self): + if ( + isinstance(self.value, Binding) + and self.gir_property is not None + and self.gir_property.construct_only + ): + raise CompileError( + f"{self.gir_property.full_name} can't be bound because it is construct-only", + hints=["construct-only properties may only be set to a static value"], + ) + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + if self.gir_property is not None: + type = self.gir_property.type + else: + type = None + + return ValueTypeCtx(type) + + @validate("name") + def property_exists(self): + if self.gir_class is None or self.gir_class.incomplete: + # Objects that we have no gir data on should not be validated + # This happens for classes defined by the app itself + return + + if self.gir_property is None: + raise CompileError( + f"Class {self.gir_class.full_name} does not have a property called {self.tokens['name']}", + did_you_mean=(self.tokens["name"], self.gir_class.properties.keys()), + ) + + @validate("name") + def property_writable(self): + if self.gir_property is not None and not self.gir_property.writable: + raise CompileError(f"{self.gir_property.full_name} is not writable") + + @validate("name") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate property '{self.tokens['name']}'", + check=lambda child: child.tokens["name"] == self.tokens["name"], + ) + + @validate("name") + def deprecated(self) -> None: + if self.gir_property is not None and self.gir_property.deprecated: + hints = [] + if self.gir_property.deprecated_doc: + hints.append(self.gir_property.deprecated_doc) + raise DeprecatedWarning( + f"{self.gir_property.signature} is deprecated", + hints=hints, + ) + + @docs("name") + def property_docs(self): + if self.gir_property is not None: + return self.gir_property.doc diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gobject_signal.py b/gtk/blueprint-compiler/blueprintcompiler/language/gobject_signal.py new file mode 100644 index 00000000000..79f9ae7c7c2 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gobject_signal.py @@ -0,0 +1,211 @@ +# gobject_signal.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import typing as T + +from .common import * +from .contexts import ScopeCtx +from .gtkbuilder_template import Template + + +class SignalFlag(AstNode): + grammar = AnyOf( + UseExact("flag", "swapped"), + UseExact("flag", "after"), + ) + + @property + def flag(self) -> str: + return self.tokens["flag"] + + @validate() + def unique(self): + self.validate_unique_in_parent( + f"Duplicate flag '{self.flag}'", lambda x: x.flag == self.flag + ) + + @docs() + def ref_docs(self): + return get_docs_section("Syntax Signal") + + +class Signal(AstNode): + grammar = Statement( + UseIdent("name"), + Optional( + [ + "::", + UseIdent("detail_name").expected("a signal detail name"), + ] + ), + Keyword("=>"), + Mark("detail_start"), + Optional(["$", UseLiteral("extern", True)]), + UseIdent("handler").expected("the name of a function to handle the signal"), + Match("(").expected("argument list"), + Optional(UseIdent("object")).expected("object identifier"), + Match(")").expected(), + ZeroOrMore(SignalFlag), + Mark("detail_end"), + ) + + @property + def name(self) -> str: + return self.tokens["name"] + + @property + def detail_name(self) -> T.Optional[str]: + return self.tokens["detail_name"] + + @property + def full_name(self) -> str: + if self.detail_name is None: + return self.name + else: + return self.name + "::" + self.detail_name + + @property + def handler(self) -> str: + return self.tokens["handler"] + + @property + def object_id(self) -> T.Optional[str]: + return self.tokens["object"] + + @property + def flags(self) -> T.List[SignalFlag]: + return self.children[SignalFlag] + + @property + def is_swapped(self) -> bool: + return any(x.flag == "swapped" for x in self.flags) + + @property + def is_after(self) -> bool: + return any(x.flag == "after" for x in self.flags) + + @property + def gir_signal(self) -> T.Optional[gir.Signal]: + if self.gir_class is not None and not isinstance(self.gir_class, ExternType): + return self.gir_class.signals.get(self.tokens["name"]) + else: + return None + + @property + def gir_class(self): + return self.parent.parent.gir_class + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.full_name, + SymbolKind.Event, + self.range, + self.group.tokens["name"].range, + self.ranges["detail_start", "detail_end"].text, + ) + + def get_reference(self, idx: int) -> T.Optional[LocationLink]: + if idx in self.group.tokens["object"].range: + obj = self.context[ScopeCtx].objects.get(self.object_id) + if obj is not None: + return LocationLink( + self.group.tokens["object"].range, obj.range, obj.ranges["id"] + ) + + return None + + @validate("handler") + def old_extern(self): + if not self.tokens["extern"]: + if self.handler is not None: + raise UpgradeWarning( + "Use the '$' extern syntax introduced in blueprint 0.8.0", + actions=[CodeAction("Use '$' syntax", "$" + self.handler)], + ) + + @validate("name") + def signal_exists(self): + if self.gir_class is None or self.gir_class.incomplete: + # Objects that we have no gir data on should not be validated + # This happens for classes defined by the app itself + return + + if self.gir_signal is None: + raise CompileError( + f"Class {self.gir_class.full_name} does not contain a signal called {self.tokens['name']}", + did_you_mean=(self.tokens["name"], self.gir_class.signals.keys()), + ) + + @validate("object") + def object_exists(self): + object_id = self.tokens["object"] + if object_id is None: + return + + if self.context[ScopeCtx].objects.get(object_id) is None: + raise CompileError(f"Could not find object with ID '{object_id}'") + + @validate("name") + def deprecated(self) -> None: + if self.gir_signal is not None and self.gir_signal.deprecated: + hints = [] + if self.gir_signal.deprecated_doc: + hints.append(self.gir_signal.deprecated_doc) + raise DeprecatedWarning( + f"{self.gir_signal.signature} is deprecated", + hints=hints, + ) + + @docs("name") + def signal_docs(self): + if self.gir_signal is not None: + return self.gir_signal.doc + + @docs("detail_name") + def detail_docs(self): + if self.name == "notify": + if self.gir_class is not None and not isinstance( + self.gir_class, ExternType + ): + prop = self.gir_class.properties.get(self.tokens["detail_name"]) + if prop is not None: + return prop.doc + + @docs("=>") + def ref_docs(self): + return get_docs_section("Syntax Signal") + + +@decompiler("signal") +def decompile_signal( + ctx, gir, name, handler, swapped="false", after="false", object=None +): + object_name = object or "" + name = name.replace("_", "-") + line = f"{name} => ${handler}({object_name})" + + if decompile.truthy(swapped): + line += " swapped" + if decompile.truthy(after): + line += " after" + + line += ";" + ctx.print(line) + return gir diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtk_a11y.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_a11y.py new file mode 100644 index 00000000000..3657565d4d1 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_a11y.py @@ -0,0 +1,289 @@ +# gtk_a11y.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import typing as T + +from ..decompiler import escape_quote +from .attributes import BaseAttribute +from .common import * +from .contexts import ValueTypeCtx +from .gobject_object import ObjectContent, validate_parent_type +from .values import Value + + +def get_property_types(gir): + # from + return { + "autocomplete": gir.get_type("AccessibleAutocomplete", "Gtk"), + "description": StringType(), + "has-popup": BoolType(), + "key-shortcuts": StringType(), + "label": StringType(), + "level": IntType(), + "modal": BoolType(), + "multi-line": BoolType(), + "multi-selectable": BoolType(), + "orientation": gir.get_type("Orientation", "Gtk"), + "placeholder": StringType(), + "read-only": BoolType(), + "required": BoolType(), + "role-description": StringType(), + "sort": gir.get_type("AccessibleSort", "Gtk"), + "value-max": FloatType(), + "value-min": FloatType(), + "value-now": FloatType(), + "value-text": StringType(), + } + + +def get_relation_types(gir): + # from + widget = gir.get_type("Widget", "Gtk") + return { + "active-descendant": widget, + "col-count": IntType(), + "col-index": IntType(), + "col-index-text": StringType(), + "col-span": IntType(), + "controls": widget, + "described-by": widget, + "details": widget, + "error-message": widget, + "flow-to": widget, + "labelled-by": widget, + "owns": widget, + "pos-in-set": IntType(), + "row-count": IntType(), + "row-index": IntType(), + "row-index-text": StringType(), + "row-span": IntType(), + "set-size": IntType(), + } + + +def get_state_types(gir): + # from + return { + "busy": BoolType(), + "checked": gir.get_type("AccessibleTristate", "Gtk"), + "disabled": BoolType(), + "expanded": BoolType(), + "hidden": BoolType(), + "invalid": gir.get_type("AccessibleInvalidState", "Gtk"), + "pressed": gir.get_type("AccessibleTristate", "Gtk"), + "selected": BoolType(), + } + + +def get_types(gir): + return { + **get_property_types(gir), + **get_relation_types(gir), + **get_state_types(gir), + } + + +allow_duplicates = [ + "controls", + "described-by", + "details", + "flow-to", + "labelled-by", + "owns", +] + + +def _get_docs(gir, name): + name = name.replace("-", "_") + if gir_type := ( + gir.get_type("AccessibleProperty", "Gtk").members.get(name) + or gir.get_type("AccessibleRelation", "Gtk").members.get(name) + or gir.get_type("AccessibleState", "Gtk").members.get(name) + ): + return gir_type.doc + + +class A11yProperty(BaseAttribute): + grammar = Statement( + UseIdent("name"), + ":", + AnyOf(Value, ["[", UseLiteral("list_form", True), Delimited(Value, ","), "]"]), + ) + + @property + def tag_name(self): + name = self.tokens["name"] + gir = self.root.gir + if name in get_property_types(gir): + return "property" + elif name in get_relation_types(gir): + return "relation" + elif name in get_state_types(gir): + return "state" + else: + raise CompilerBugError() + + @property + def name(self): + return self.tokens["name"].replace("_", "-") + + @property + def values(self) -> T.List[Value]: + return list(self.children) + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(get_types(self.root.gir).get(self.tokens["name"])) + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.name, + SymbolKind.Field, + self.range, + self.group.tokens["name"].range, + ", ".join(v.range.text for v in self.values), + ) + + @validate("name") + def is_valid_property(self): + types = get_types(self.root.gir) + if self.tokens["name"] not in types: + raise CompileError( + f"'{self.tokens['name']}' is not an accessibility property, relation, or state", + did_you_mean=(self.tokens["name"], types.keys()), + ) + + @validate("name") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate accessibility attribute '{self.tokens['name']}'", + check=lambda child: child.tokens["name"] == self.tokens["name"], + ) + + @validate("name") + def list_only_allowed_for_subset(self): + if self.tokens["list_form"] and self.tokens["name"] not in allow_duplicates: + raise CompileError( + f"'{self.tokens['name']}' does not allow a list of values", + ) + + @validate("name") + def list_non_empty(self): + if len(self.values) == 0: + raise CompileError( + f"'{self.tokens['name']}' may not be empty", + ) + + @docs("name") + def prop_docs(self): + if self.tokens["name"] in get_types(self.root.gir): + return _get_docs(self.root.gir, self.tokens["name"]) + + +class ExtAccessibility(AstNode): + grammar = [ + Keyword("accessibility"), + "{", + Until(A11yProperty, "}"), + ] + + @property + def properties(self) -> T.List[A11yProperty]: + return self.children[A11yProperty] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "accessibility", + SymbolKind.Struct, + self.range, + self.group.tokens["accessibility"].range, + ) + + @validate("accessibility") + def container_is_widget(self): + validate_parent_type(self, "Gtk", "Widget", "accessibility properties") + + @validate("accessibility") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate accessibility block") + + @docs("accessibility") + def ref_docs(self): + return get_docs_section("Syntax ExtAccessibility") + + +@completer( + applies_in=[ObjectContent], + matches=new_statement_patterns, +) +def a11y_completer(lsp, ast_node, match_variables): + yield Completion( + "accessibility", CompletionItemKind.Snippet, snippet="accessibility {\n $0\n}" + ) + + +@completer( + applies_in=[ExtAccessibility], + matches=new_statement_patterns, +) +def a11y_name_completer(lsp, ast_node, match_variables): + for name, type in get_types(ast_node.root.gir).items(): + yield Completion( + name, + CompletionItemKind.Property, + docs=_get_docs(ast_node.root.gir, type.name), + ) + + +@decompiler("accessibility", skip_children=True, element=True) +def decompile_accessibility(ctx: DecompileCtx, _gir, element): + ctx.print("accessibility {") + already_printed = set() + types = get_types(ctx.gir) + + for child in element.children: + name = child["name"] + + if name in allow_duplicates: + if name in already_printed: + continue + + ctx.print(f"{name}: [") + for value in element.children: + if value["name"] == name: + comments, string = ctx.decompile_value( + value.cdata, + types.get(value["name"]), + (value["translatable"], value["context"], value["comments"]), + ) + ctx.print(f"{comments} {string},") + ctx.print("];") + else: + comments, string = ctx.decompile_value( + child.cdata, + types.get(child["name"]), + (child["translatable"], child["context"], child["comments"]), + ) + ctx.print(f"{comments} {name}: {string};") + + already_printed.add(name) + ctx.print("}") + ctx.end_block_with("") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtk_combo_box_text.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_combo_box_text.py new file mode 100644 index 00000000000..312750a1648 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_combo_box_text.py @@ -0,0 +1,125 @@ +# gtk_combo_box_text.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .common import * +from .contexts import ValueTypeCtx +from .gobject_object import ObjectContent, validate_parent_type +from .values import StringValue + + +class Item(AstNode): + grammar = [ + Optional([UseIdent("name"), ":"]), + StringValue, + ] + + @property + def name(self) -> T.Optional[str]: + return self.tokens["name"] + + @property + def value(self) -> StringValue: + return self.children[StringValue][0] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.value.range.text, + SymbolKind.String, + self.range, + self.value.range, + self.name, + ) + + @validate("name") + def unique_in_parent(self): + if self.name is not None: + self.validate_unique_in_parent( + f"Duplicate item '{self.name}'", lambda x: x.name == self.name + ) + + @docs("name") + def ref_docs(self): + return get_docs_section("Syntax ExtComboBoxItems") + + +class ExtComboBoxItems(AstNode): + grammar = [ + Keyword("items"), + "[", + Delimited(Item, ","), + "]", + ] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "items", + SymbolKind.Array, + self.range, + self.group.tokens["items"].range, + ) + + @validate("items") + def container_is_combo_box_text(self): + validate_parent_type(self, "Gtk", "ComboBoxText", "combo box items") + + @validate("items") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate items block") + + @docs("items") + def ref_docs(self): + return get_docs_section("Syntax ExtComboBoxItems") + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Gtk", "ComboBoxText"), + matches=new_statement_patterns, +) +def items_completer(lsp, ast_node, match_variables): + yield Completion("items", CompletionItemKind.Snippet, snippet="items [$0]") + + +@decompiler("items", parent_type="Gtk.ComboBoxText") +def decompile_items(ctx: DecompileCtx, gir: gir.GirContext): + ctx.print("items [") + + +@decompiler("item", parent_type="Gtk.ComboBoxText", cdata=True) +def decompile_item( + ctx: DecompileCtx, + gir: gir.GirContext, + cdata: str, + id: T.Optional[str] = None, + translatable="false", + comments=None, + context=None, +): + comments, translatable = decompile_translatable( + cdata, translatable, context, comments + ) + if comments: + ctx.print(comments) + if id: + ctx.print(f"{id}: ") + ctx.print(translatable) + ctx.print(",") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtk_file_filter.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_file_filter.py new file mode 100644 index 00000000000..e84afc7fa63 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_file_filter.py @@ -0,0 +1,139 @@ +# gtk_file_filter.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .common import * +from .gobject_object import ObjectContent, validate_parent_type + + +class Filters(AstNode): + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.tokens["tag_name"], + SymbolKind.Array, + self.range, + self.group.tokens["tag_name"].range, + ) + + @validate() + def container_is_file_filter(self): + validate_parent_type(self, "Gtk", "FileFilter", "file filter properties") + + @validate("tag_name") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate {self.tokens['tag_name']} block", + check=lambda child: child.tokens["tag_name"] == self.tokens["tag_name"], + ) + + @docs("tag_name") + def ref_docs(self): + return get_docs_section("Syntax ExtFileFilter") + + +class FilterString(AstNode): + @property + def item(self) -> str: + return self.tokens["name"] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.item, + SymbolKind.String, + self.range, + self.group.tokens["name"].range, + ) + + @validate() + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate {self.tokens['tag_name']} '{self.item}'", + check=lambda child: child.item == self.item, + ) + + +def create_node(tag_name: str, singular: str): + return Group( + Filters, + [ + UseExact("tag_name", tag_name), + "[", + Delimited( + Group( + FilterString, + [ + UseQuoted("name"), + UseLiteral("tag_name", singular), + ], + ), + ",", + ), + "]", + ], + ) + + +ext_file_filter_mime_types = create_node("mime-types", "mime-type") +ext_file_filter_patterns = create_node("patterns", "pattern") +ext_file_filter_suffixes = create_node("suffixes", "suffix") + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Gtk", "FileFilter"), + matches=new_statement_patterns, +) +def file_filter_completer(lsp, ast_node, match_variables): + yield Completion( + "mime-types", CompletionItemKind.Snippet, snippet='mime-types ["$0"]' + ) + yield Completion("patterns", CompletionItemKind.Snippet, snippet='patterns ["$0"]') + yield Completion("suffixes", CompletionItemKind.Snippet, snippet='suffixes ["$0"]') + + +@decompiler("mime-types") +def decompile_mime_types(ctx, gir): + ctx.print("mime-types [") + + +@decompiler("mime-type", cdata=True) +def decompile_mime_type(ctx, gir, cdata): + ctx.print(f"{escape_quote(cdata)},") + + +@decompiler("patterns") +def decompile_patterns(ctx, gir): + ctx.print("patterns [") + + +@decompiler("pattern", cdata=True) +def decompile_pattern(ctx, gir, cdata): + ctx.print(f"{escape_quote(cdata)},") + + +@decompiler("suffixes") +def decompile_suffixes(ctx, gir): + ctx.print("suffixes [") + + +@decompiler("suffix", cdata=True) +def decompile_suffix(ctx, gir, cdata): + ctx.print(f"{escape_quote(cdata)},") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtk_layout.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_layout.py new file mode 100644 index 00000000000..8d3e37a0f58 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_layout.py @@ -0,0 +1,102 @@ +# gtk_layout.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .common import * +from .contexts import ValueTypeCtx +from .gobject_object import ObjectContent, validate_parent_type +from .values import Value + + +class LayoutProperty(AstNode): + grammar = Statement(UseIdent("name"), ":", Err(Value, "Expected a value")) + tag_name = "property" + + @property + def name(self) -> str: + return self.tokens["name"] + + @property + def value(self) -> Value: + return self.children[Value][0] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.name, + SymbolKind.Field, + self.range, + self.group.tokens["name"].range, + self.value.range.text, + ) + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + # there isn't really a way to validate these + return ValueTypeCtx(None) + + @validate("name") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate layout property '{self.name}'", + check=lambda child: child.name == self.name, + ) + + +class ExtLayout(AstNode): + grammar = Sequence( + Keyword("layout"), + "{", + Until(LayoutProperty, "}"), + ) + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "layout", + SymbolKind.Struct, + self.range, + self.group.tokens["layout"].range, + ) + + @validate("layout") + def container_is_widget(self): + validate_parent_type(self, "Gtk", "Widget", "layout properties") + + @validate("layout") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate layout block") + + @docs("layout") + def ref_docs(self): + return get_docs_section("Syntax ExtLayout") + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Gtk", "Widget"), + matches=new_statement_patterns, +) +def layout_completer(lsp, ast_node, match_variables): + yield Completion("layout", CompletionItemKind.Snippet, snippet="layout {\n $0\n}") + + +@decompiler("layout") +def decompile_layout(ctx, gir): + ctx.print("layout {") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtk_list_item_factory.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_list_item_factory.py new file mode 100644 index 00000000000..3309c08f043 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_list_item_factory.py @@ -0,0 +1,114 @@ +import typing as T + +from blueprintcompiler.errors import T +from blueprintcompiler.lsp_utils import DocumentSymbol + +from ..ast_utils import AstNode, validate +from .common import * +from .contexts import ScopeCtx +from .gobject_object import ObjectContent, validate_parent_type +from .types import TypeName + + +class ExtListItemFactory(AstNode): + grammar = [ + UseExact("id", "template"), + Mark("typename_start"), + Optional(TypeName), + Mark("typename_end"), + ObjectContent, + ] + + @property + def id(self) -> str: + return "template" + + @property + def signature(self) -> str: + return f"template {self.gir_class.full_name}" + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.signature, + SymbolKind.Object, + self.range, + self.group.tokens["id"].range, + ) + + @property + def type_name(self) -> T.Optional[TypeName]: + if len(self.children[TypeName]) == 1: + return self.children[TypeName][0] + else: + return None + + @property + def gir_class(self): + if self.type_name is not None: + return self.type_name.gir_type + else: + return self.root.gir.get_type("ListItem", "Gtk") + + @validate("template") + def container_is_builder_list(self): + validate_parent_type( + self, + "Gtk", + "BuilderListItemFactory", + "sub-templates", + ) + + @validate("template") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate template block") + + @validate("typename_start", "typename_end") + def type_is_list_item(self): + if self.type_name is not None: + if self.type_name.glib_type_name not in ( + "GtkListItem", + "GtkListHeader", + "GtkColumnViewRow", + "GtkColumnViewCell", + ): + raise CompileError( + f"Only Gtk.ListItem, Gtk.ListHeader, Gtk.ColumnViewRow, or Gtk.ColumnViewCell is allowed as a type here" + ) + + @validate("template") + def type_name_upgrade(self): + if self.type_name is None: + raise UpgradeWarning( + "Expected type name after 'template' keyword", + actions=[ + CodeAction( + "Add ListItem type to template block (introduced in blueprint 0.8.0)", + "template ListItem", + ) + ], + ) + + @context(ScopeCtx) + def scope_ctx(self) -> ScopeCtx: + return ScopeCtx(node=self) + + @validate() + def unique_ids(self): + self.context[ScopeCtx].validate_unique_ids() + + @property + def content(self) -> ObjectContent: + return self.children[ObjectContent][0] + + @property + def action_widgets(self): + """ + The sub-template shouldn't have it`s own actions this is + just hear to satisfy XmlOutput._emit_object_or_template + """ + return None + + @docs("id") + def ref_docs(self): + return get_docs_section("Syntax ExtListItemFactory") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtk_menu.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_menu.py new file mode 100644 index 00000000000..c7ef5f2bc09 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_menu.py @@ -0,0 +1,302 @@ +# gtk_menus.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import typing as T + +from blueprintcompiler.language.values import StringValue + +from .common import * +from .contexts import ValueTypeCtx +from .gobject_object import RESERVED_IDS + + +class Menu(AstNode): + @property + def gir_class(self): + return self.root.gir.namespaces["Gtk"].lookup_type("Gio.Menu") + + @property + def id(self) -> str: + return self.tokens["id"] + + @property + def signature(self) -> str: + if self.id: + return f"Gio.Menu {self.id}" + else: + return "Gio.Menu" + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.tokens["tag"], + SymbolKind.Object, + self.range, + self.group.tokens[self.tokens["tag"]].range, + self.id, + ) + + @property + def tag(self) -> str: + return self.tokens["tag"] + + @property + def items(self) -> T.List[T.Union["Menu", "MenuAttribute"]]: + return self.children + + @validate("menu") + def has_id(self): + if self.tokens["tag"] == "menu" and self.tokens["id"] is None: + raise CompileError("Menu requires an ID") + + @validate("id") + def object_id_not_reserved(self): + if self.id in RESERVED_IDS: + raise CompileWarning(f"{self.id} may be a confusing object ID") + + @docs("menu") + def ref_docs_menu(self): + return get_docs_section("Syntax Menu") + + @docs("section") + def ref_docs_section(self): + return get_docs_section("Syntax Menu") + + @docs("submenu") + def ref_docs_submenu(self): + return get_docs_section("Syntax Menu") + + @docs("item") + def ref_docs_item(self): + if self.tokens["shorthand"]: + return get_docs_section("Syntax MenuItemShorthand") + else: + return get_docs_section("Syntax Menu") + + +class MenuAttribute(AstNode): + tag_name = "attribute" + + @property + def name(self) -> str: + return self.tokens["name"] + + @property + def value(self) -> StringValue: + return self.children[StringValue][0] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.name, + SymbolKind.Field, + self.range, + ( + self.group.tokens["name"].range + if self.group.tokens["name"] + else self.range + ), + self.value.range.text, + ) + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(None) + + @validate("name") + def unique(self): + self.validate_unique_in_parent( + f"Duplicate attribute '{self.name}'", lambda x: x.name == self.name + ) + + +menu_child = AnyOf() + +menu_attribute = Group( + MenuAttribute, + [ + UseIdent("name"), + ":", + Err(StringValue, "Expected string or translated string"), + Match(";").expected(), + ], +) + +menu_section = Group( + Menu, + [ + Keyword("section"), + UseLiteral("tag", "section"), + Optional(UseIdent("id")), + Match("{").expected(), + Until(AnyOf(menu_child, menu_attribute), "}"), + ], +) + +menu_submenu = Group( + Menu, + [ + Keyword("submenu"), + UseLiteral("tag", "submenu"), + Optional(UseIdent("id")), + Match("{").expected(), + Until(AnyOf(menu_child, menu_attribute), "}"), + ], +) + +menu_item = Group( + Menu, + [ + Keyword("item"), + UseLiteral("tag", "item"), + Match("{").expected(), + Until(menu_attribute, "}"), + ], +) + +menu_item_shorthand = Group( + Menu, + [ + Keyword("item"), + UseLiteral("tag", "item"), + UseLiteral("shorthand", True), + "(", + Group( + MenuAttribute, + [UseLiteral("name", "label"), StringValue], + ), + Optional( + [ + ",", + Optional( + [ + Group( + MenuAttribute, + [UseLiteral("name", "action"), StringValue], + ), + Optional( + [ + ",", + Group( + MenuAttribute, + [UseLiteral("name", "icon"), StringValue], + ), + ] + ), + ] + ), + ] + ), + Match(")").expected(), + ], +) + +menu_child.children = [ + menu_section, + menu_submenu, + menu_item_shorthand, + menu_item, +] + +menu: Group = Group( + Menu, + [ + Keyword("menu"), + UseLiteral("tag", "menu"), + Optional(UseIdent("id")), + [ + Match("{"), + Until( + AnyOf( + menu_child, + Fail( + menu_attribute, + "Attributes are not permitted at the top level of a menu", + ), + ), + "}", + ), + ], + ], +) + +from .ui import UI + + +@completer( + applies_in=[UI], + matches=new_statement_patterns, +) +def menu_completer(lsp, ast_node, match_variables): + yield Completion("menu", CompletionItemKind.Snippet, snippet="menu {\n $0\n}") + + +@completer( + applies_in=[Menu], + matches=new_statement_patterns, +) +def menu_content_completer(lsp, ast_node, match_variables): + yield Completion( + "submenu", CompletionItemKind.Snippet, snippet="submenu {\n $0\n}" + ) + yield Completion( + "section", CompletionItemKind.Snippet, snippet="section {\n $0\n}" + ) + yield Completion("item", CompletionItemKind.Snippet, snippet="item {\n $0\n}") + yield Completion( + "item (shorthand)", + CompletionItemKind.Snippet, + snippet='item (_("${1:Label}"), "${2:action-name}", "${3:icon-name}")', + ) + + yield Completion("label", CompletionItemKind.Snippet, snippet="label: $0;") + yield Completion("action", CompletionItemKind.Snippet, snippet='action: "$0";') + yield Completion("icon", CompletionItemKind.Snippet, snippet='icon: "$0";') + + +@decompiler("menu") +def decompile_menu(ctx, gir, id=None): + if id: + ctx.print(f"menu {id} {{") + else: + ctx.print("menu {") + + +@decompiler("submenu") +def decompile_submenu(ctx, gir, id=None): + if id: + ctx.print(f"submenu {id} {{") + else: + ctx.print("submenu {") + + +@decompiler("item", parent_tag="menu") +def decompile_item(ctx, gir, id=None): + if id: + ctx.print(f"item {id} {{") + else: + ctx.print("item {") + + +@decompiler("section") +def decompile_section(ctx, gir, id=None): + if id: + ctx.print(f"section {id} {{") + else: + ctx.print("section {") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtk_scale.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_scale.py new file mode 100644 index 00000000000..1fd5ac3dea0 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_scale.py @@ -0,0 +1,187 @@ +# gtk_scale.py +# +# Copyright 2023 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +from .common import * +from .gobject_object import ObjectContent, validate_parent_type +from .values import StringValue + + +class ExtScaleMark(AstNode): + grammar = [ + Keyword("mark"), + Match("(").expected(), + [ + Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))), + UseNumber("value"), + Optional( + [ + ",", + UseIdent("position"), + Optional([",", StringValue]), + ] + ), + ], + Match(")").expected(), + ] + + @property + def value(self) -> float: + if self.tokens["sign"] == "-": + return -self.tokens["value"] + else: + return self.tokens["value"] + + @property + def position(self) -> T.Optional[str]: + return self.tokens["position"] + + @property + def label(self) -> T.Optional[StringValue]: + if len(self.children[StringValue]) == 1: + return self.children[StringValue][0] + else: + return None + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + str(self.value), + SymbolKind.Field, + self.range, + self.group.tokens["mark"].range, + self.label.string if self.label else None, + ) + + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: + if range := self.ranges["position"]: + yield SemanticToken( + range.start, + range.end, + SemanticTokenType.EnumMember, + ) + + @docs("position") + def position_docs(self) -> T.Optional[str]: + if member := self.root.gir.get_type("PositionType", "Gtk").members.get( + self.position + ): + return member.doc + else: + return None + + @validate("position") + def validate_position(self): + positions = self.root.gir.get_type("PositionType", "Gtk").members + if self.position is not None and positions.get(self.position) is None: + raise CompileError( + f"'{self.position}' is not a member of Gtk.PositionType", + did_you_mean=(self.position, positions.keys()), + ) + + @docs("mark") + def ref_docs(self): + return get_docs_section("Syntax ExtScaleMarks") + + +class ExtScaleMarks(AstNode): + grammar = [ + Keyword("marks"), + Match("[").expected(), + Until(ExtScaleMark, "]", ","), + ] + + @property + def marks(self) -> T.List[ExtScaleMark]: + return self.children + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "marks", + SymbolKind.Array, + self.range, + self.group.tokens["marks"].range, + ) + + @validate("marks") + def container_is_size_group(self): + validate_parent_type(self, "Gtk", "Scale", "scale marks") + + @validate("marks") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate 'marks' block") + + @docs("marks") + def ref_docs(self): + return get_docs_section("Syntax ExtScaleMarks") + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Gtk", "Scale"), + matches=new_statement_patterns, +) +def complete_marks(lsp, ast_node, match_variables): + yield Completion("marks", CompletionItemKind.Keyword, snippet="marks [\n\t$0\n]") + + +@completer( + applies_in=[ExtScaleMarks], +) +def complete_mark(lsp, ast_node, match_variables): + yield Completion("mark", CompletionItemKind.Keyword, snippet="mark ($0),") + + +@decompiler("marks") +def decompile_marks( + ctx, + gir, +): + ctx.print("marks [") + + +@decompiler("mark", cdata=True) +def decompile_mark( + ctx: DecompileCtx, + gir, + value, + position=None, + cdata=None, + translatable="false", + comments=None, + context=None, +): + if comments is not None: + ctx.print(f"/* Translators: {comments} */") + + text = f"mark ({value}" + + if position: + text += f", {position}" + elif cdata: + text += f", bottom" + + if truthy(translatable): + comments, translatable = decompile_translatable( + cdata, translatable, context, comments + ) + text += f", {translatable}" + + text += ")," + ctx.print(text) diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtk_size_group.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_size_group.py new file mode 100644 index 00000000000..54d85e5a5a4 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_size_group.py @@ -0,0 +1,118 @@ +# gtk_size_group.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .common import * +from .contexts import ScopeCtx +from .gobject_object import ObjectContent, validate_parent_type + + +class Widget(AstNode): + grammar = UseIdent("name") + + @property + def name(self) -> str: + return self.tokens["name"] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.name, + SymbolKind.Field, + self.range, + self.group.tokens["name"].range, + ) + + def get_reference(self, _idx: int) -> T.Optional[LocationLink]: + if obj := self.context[ScopeCtx].objects.get(self.name): + return LocationLink(self.range, obj.range, obj.ranges["id"]) + else: + return None + + @validate("name") + def obj_widget(self): + object = self.context[ScopeCtx].objects.get(self.tokens["name"]) + type = self.root.gir.get_type("Widget", "Gtk") + if object is None: + raise CompileError( + f"Could not find object with ID {self.tokens['name']}", + did_you_mean=( + self.tokens["name"], + self.context[ScopeCtx].objects.keys(), + ), + ) + elif object.gir_class and not object.gir_class.assignable_to(type): + raise CompileError( + f"Cannot assign {object.gir_class.full_name} to {type.full_name}" + ) + + @validate("name") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Object '{self.name}' is listed twice", lambda x: x.name == self.name + ) + + +class ExtSizeGroupWidgets(AstNode): + grammar = [ + Keyword("widgets"), + "[", + Delimited(Widget, ","), + "]", + ] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "widgets", + SymbolKind.Array, + self.range, + self.group.tokens["widgets"].range, + ) + + @validate("widgets") + def container_is_size_group(self): + validate_parent_type(self, "Gtk", "SizeGroup", "size group properties") + + @validate("widgets") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate widgets block") + + @docs("widgets") + def ref_docs(self): + return get_docs_section("Syntax ExtSizeGroupWidgets") + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Gtk", "SizeGroup"), + matches=new_statement_patterns, +) +def size_group_completer(lsp, ast_node, match_variables): + yield Completion("widgets", CompletionItemKind.Snippet, snippet="widgets [$0]") + + +@decompiler("widgets") +def size_group_decompiler(ctx, gir: gir.GirContext): + ctx.print("widgets [") + + +@decompiler("widget") +def widget_decompiler(ctx, gir: gir.GirContext, name: str): + ctx.print(name + ",") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtk_string_list.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_string_list.py new file mode 100644 index 00000000000..a146f3535b2 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_string_list.py @@ -0,0 +1,101 @@ +# gtk_combo_box_text.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .common import * +from .gobject_object import ObjectContent, validate_parent_type +from .values import StringValue + + +class Item(AstNode): + grammar = StringValue + + @property + def child(self) -> StringValue: + return self.children[StringValue][0] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.child.range.text, + SymbolKind.String, + self.range, + self.range, + ) + + +class ExtStringListStrings(AstNode): + grammar = [ + Keyword("strings"), + "[", + Delimited(Item, ","), + "]", + ] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "strings", + SymbolKind.Array, + self.range, + self.group.tokens["strings"].range, + ) + + @validate("strings") + def container_is_string_list(self): + validate_parent_type(self, "Gtk", "StringList", "StringList items") + + @validate("strings") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate strings block") + + @docs("strings") + def ref_docs(self): + return get_docs_section("Syntax ExtStringListStrings") + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Gtk", "StringList"), + matches=new_statement_patterns, +) +def strings_completer(lsp, ast_node, match_variables): + yield Completion("strings", CompletionItemKind.Snippet, snippet="strings [$0]") + + +@decompiler("items", parent_type="Gtk.StringList") +def decompile_strings(ctx: DecompileCtx, gir: gir.GirContext): + ctx.print("strings [") + + +@decompiler("item", cdata=True, parent_type="Gtk.StringList") +def decompile_item( + ctx: DecompileCtx, + gir: gir.GirContext, + translatable="false", + comments=None, + context=None, + cdata=None, +): + comments, translatable = decompile_translatable( + cdata, translatable, context, comments + ) + if comments is not None: + ctx.print(comments) + ctx.print(translatable + ",") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtk_styles.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_styles.py new file mode 100644 index 00000000000..86175225dd5 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtk_styles.py @@ -0,0 +1,94 @@ +# gtk_styles.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .common import * +from .gobject_object import ObjectContent, validate_parent_type + + +class StyleClass(AstNode): + grammar = UseQuoted("name") + + @property + def name(self) -> str: + return self.tokens["name"] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.name, + SymbolKind.String, + self.range, + self.range, + ) + + @validate("name") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Duplicate style class '{self.name}'", lambda x: x.name == self.name + ) + + +class ExtStyles(AstNode): + grammar = [ + Keyword("styles"), + "[", + Delimited(StyleClass, ","), + "]", + ] + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + "styles", + SymbolKind.Array, + self.range, + self.group.tokens["styles"].range, + ) + + @validate("styles") + def container_is_widget(self): + validate_parent_type(self, "Gtk", "Widget", "style classes") + + @validate("styles") + def unique_in_parent(self): + self.validate_unique_in_parent("Duplicate styles block") + + @docs("styles") + def ref_docs(self): + return get_docs_section("Syntax ExtStyles") + + +@completer( + applies_in=[ObjectContent], + applies_in_subclass=("Gtk", "Widget"), + matches=new_statement_patterns, +) +def style_completer(lsp, ast_node, match_variables): + yield Completion("styles", CompletionItemKind.Keyword, snippet='styles ["$0"]') + + +@decompiler("style") +def decompile_style(ctx, gir): + ctx.print(f"styles [") + + +@decompiler("class") +def decompile_style_class(ctx, gir, name): + ctx.print(f'"{name}",') diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtkbuilder_child.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtkbuilder_child.py new file mode 100644 index 00000000000..bee551c0710 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtkbuilder_child.py @@ -0,0 +1,145 @@ +# gtkbuilder_child.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from functools import cached_property + +from .common import * +from .gobject_object import Object +from .response_id import ExtResponse, decompile_response_type + +ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [ + ("Gtk", "Buildable"), + ("Gio", "ListStore"), +] + + +class ChildInternal(AstNode): + grammar = ["internal-child", UseIdent("internal_child")] + + @property + def internal_child(self) -> str: + return self.tokens["internal_child"] + + +class ChildType(AstNode): + grammar = UseIdent("child_type").expected("a child type") + + @property + def child_type(self) -> str: + return self.tokens["child_type"] + + +class ChildExtension(AstNode): + grammar = ExtResponse + + @property + def child(self) -> ExtResponse: + return self.children[0] + + @docs() + def ref_docs(self): + return get_docs_section("Syntax ChildExtension") + + +class ChildAnnotation(AstNode): + grammar = ["[", AnyOf(ChildInternal, ChildExtension, ChildType), "]"] + + @property + def child(self) -> T.Union[ChildInternal, ChildExtension, ChildType]: + return self.children[0] + + +class Child(AstNode): + grammar = [ + Optional(ChildAnnotation), + Object, + ] + + @property + def annotation(self) -> T.Optional[ChildAnnotation]: + annotations = self.children[ChildAnnotation] + return annotations[0] if len(annotations) else None + + @property + def object(self) -> Object: + return self.children[Object][0] + + @validate() + def parent_can_have_child(self): + if gir_class := self.parent.gir_class: + for namespace, name in ALLOWED_PARENTS: + parent_type = self.root.gir.get_type(name, namespace) + if gir_class.assignable_to(parent_type): + break + else: + hints = [ + "only Gio.ListStore or Gtk.Buildable implementors can have children" + ] + if hasattr(gir_class, "properties") and "child" in gir_class.properties: + hints.append( + "did you mean to assign this object to the 'child' property?" + ) + raise CompileError( + f"{gir_class.full_name} doesn't have children", + hints=hints, + ) + + @cached_property + def response_id(self) -> T.Optional[ExtResponse]: + """Get action widget's response ID. + + If child is not action widget, returns `None`. + """ + if ( + self.annotation is not None + and isinstance(self.annotation.child, ChildExtension) + and isinstance(self.annotation.child.child, ExtResponse) + ): + return self.annotation.child.child + else: + return None + + @validate() + def internal_child_unique(self): + if self.annotation is not None: + if isinstance(self.annotation.child, ChildInternal): + internal_child = self.annotation.child.internal_child + self.validate_unique_in_parent( + f"Duplicate internal child '{internal_child}'", + lambda x: ( + x.annotation + and isinstance(x.annotation.child, ChildInternal) + and x.annotation.child.internal_child == internal_child + ), + ) + + +@decompiler("child", element=True) +def decompile_child(ctx, gir, element): + if type := element["type"]: + if type == "action": + if decompiled := decompile_response_type(ctx.parent_node, element): + ctx.print(decompiled) + return + + ctx.print(f"[{type}]") + elif internal_child := element["internal-child"]: + ctx.print(f"[internal-child {internal_child}]") + return gir diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/gtkbuilder_template.py b/gtk/blueprint-compiler/blueprintcompiler/language/gtkbuilder_template.py new file mode 100644 index 00000000000..96383eb84d8 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/gtkbuilder_template.py @@ -0,0 +1,109 @@ +# gtkbuilder_template.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import typing as T + +from blueprintcompiler.language.common import GirType + +from ..gir import TemplateType +from .common import * +from .gobject_object import Object, ObjectContent +from .types import ClassName, TemplateClassName + + +class Template(Object): + grammar = [ + UseExact("id", "template"), + to_parse_node(TemplateClassName).expected("template type"), + Optional( + [ + Match(":"), + to_parse_node(ClassName).expected("parent class"), + ] + ), + ObjectContent, + ] + + @property + def id(self) -> str: + return "template" + + @property + def signature(self) -> str: + if self.parent_type and self.parent_type.gir_type: + return f"template {self.class_name.as_string} : {self.parent_type.gir_type.full_name}" + else: + return f"template {self.class_name.as_string}" + + @property + def document_symbol(self) -> DocumentSymbol: + return DocumentSymbol( + self.signature, + SymbolKind.Object, + self.range, + self.group.tokens["id"].range, + ) + + @property + def gir_class(self) -> GirType: + if isinstance(self.class_name.gir_type, ExternType): + if gir := self.parent_type: + return TemplateType(self.class_name.gir_type.full_name, gir.gir_type) + return self.class_name.gir_type + + @property + def parent_type(self) -> T.Optional[ClassName]: + if len(self.children[ClassName]) == 2: + return self.children[ClassName][1] + else: + return None + + @validate() + def parent_only_if_extern(self): + if not isinstance(self.class_name.gir_type, ExternType): + if self.parent_type is not None: + raise CompileError( + "Parent type may only be specified if the template type is extern" + ) + + @validate("id") + def unique_in_parent(self): + self.validate_unique_in_parent( + f"Only one template may be defined per file, but this file contains {len(self.parent.children[Template])}", + ) + + @docs("id") + def ref_docs(self): + return get_docs_section("Syntax Template") + + +@decompiler("template") +def decompile_template(ctx: DecompileCtx, gir, klass, parent=None): + def class_name(cname: str) -> str: + if gir := ctx.type_by_cname(cname): + return decompile.full_name(gir) + else: + return "$" + cname + + if parent is None: + ctx.print(f"template {class_name(klass)} {{") + else: + ctx.print(f"template {class_name(klass)} : {class_name(parent)} {{") + + return ctx.type_by_cname(klass) or ctx.type_by_cname(parent) diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/imports.py b/gtk/blueprint-compiler/blueprintcompiler/language/imports.py new file mode 100644 index 00000000000..3060bea9f24 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/imports.py @@ -0,0 +1,115 @@ +# imports.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .. import gir +from .common import * + + +class GtkDirective(AstNode): + grammar = Statement( + Match("using").err( + 'File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`)' + ), + Match("Gtk").err( + 'File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`)' + ), + UseNumberText("version").expected("a version number for GTK"), + ) + + @validate("version") + def gtk_version(self): + version = self.tokens["version"] + if version not in ["4.0"]: + err = CompileError("Only GTK 4 is supported") + if version and version.startswith("4"): + err.hint( + "Expected the GIR version, not an exact version number. Use 'using Gtk 4.0;'." + ) + else: + err.hint("Expected 'using Gtk 4.0;'") + raise err + + try: + gir.get_namespace("Gtk", version) + except CompileError as e: + raise CompileError( + "Could not find GTK 4 introspection files. Is gobject-introspection installed?", + fatal=True, + # preserve the hints from the original error, because it contains + # useful debugging information + hints=e.hints, + ) + + @property + def gir_namespace(self): + # validate the GTK version first to make sure the more specific error + # message is emitted + self.gtk_version() + if self.tokens["version"] is not None: + return gir.get_namespace("Gtk", self.tokens["version"]) + else: + # For better error handling, just assume it's 4.0 + return gir.get_namespace("Gtk", "4.0") + + @docs() + def ref_docs(self): + return get_docs_section("Syntax GtkDecl") + + +class Import(AstNode): + grammar = Statement( + "using", + UseIdent("namespace").expected("a GIR namespace"), + UseNumberText("version").expected("a version number"), + ) + + @property + def namespace(self): + return self.tokens["namespace"] + + @property + def version(self): + return self.tokens["version"] + + @validate("namespace", "version") + def namespace_exists(self): + gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) + + @validate() + def unused(self): + if self.namespace not in self.root.used_imports: + raise UnusedWarning( + f"Unused import: {self.namespace}", + self.range, + actions=[ + CodeAction("Remove import", "", self.range.with_trailing_newline) + ], + ) + + @property + def gir_namespace(self): + try: + return gir.get_namespace(self.tokens["namespace"], self.tokens["version"]) + except CompileError: + return None + + @docs() + def ref_docs(self): + return get_docs_section("Syntax Using") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/response_id.py b/gtk/blueprint-compiler/blueprintcompiler/language/response_id.py new file mode 100644 index 00000000000..939f71f28ce --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/response_id.py @@ -0,0 +1,164 @@ +# response_id.py +# +# Copyright 2022 Gleb Smirnov +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +import typing as T + +from .common import * + + +class ExtResponse(AstNode): + """Response ID of action widget.""" + + ALLOWED_PARENTS: T.List[T.Tuple[str, str]] = [("Gtk", "Dialog"), ("Gtk", "InfoBar")] + + grammar = [ + Keyword("action"), + Keyword("response"), + "=", + AnyOf( + UseIdent("response_id"), + [ + Optional(UseExact("sign", "-")), + UseNumber("response_id"), + ], + ), + Optional([Keyword("default"), UseLiteral("is_default", True)]), + ] + + @validate() + def parent_has_action_widgets(self) -> None: + """Chech that parent widget has allowed type.""" + from .gobject_object import Object + + container_type = self.parent_by_type(Object).gir_class + if container_type is None: + return + + gir = self.root.gir + + for namespace, name in ExtResponse.ALLOWED_PARENTS: + parent_type = gir.get_type(name, namespace) + if container_type.assignable_to(parent_type): + break + else: + raise CompileError( + f"{container_type.full_name} doesn't have action widgets" + ) + + @validate() + def widget_have_id(self) -> None: + """Check that action widget have ID.""" + from .gtkbuilder_child import Child + + object = self.parent_by_type(Child).object + if object.id is None: + raise CompileError(f"Action widget must have ID") + + @validate("response_id") + def correct_response_type(self) -> None: + """Validate response type. + + Response type might be GtkResponseType member + or positive number. + """ + gir = self.root.gir + response = self.tokens["response_id"] + + if self.tokens["sign"] == "-": + raise CompileError("Numeric response type can't be negative") + + if isinstance(response, float): + raise CompileError( + "Response type must be GtkResponseType member or integer," " not float" + ) + elif not isinstance(response, int): + responses = gir.get_type("ResponseType", "Gtk").members.keys() + if response not in responses: + raise CompileError(f'Response type "{response}" doesn\'t exist') + + @validate("default") + def no_multiple_default(self) -> None: + """Only one action widget in dialog can be default.""" + from .gobject_object import Object + + if not self.is_default: + return + + action_widgets = self.parent_by_type(Object).action_widgets + for widget in action_widgets: + if widget == self: + break + if widget.tokens["is_default"]: + raise CompileError("Default response is already set") + + @property + def response_id(self) -> str: + return self.tokens["response_id"] + + @property + def is_default(self) -> bool: + return self.tokens["is_default"] or False + + @property + def widget_id(self) -> str: + """Get action widget ID.""" + from .gtkbuilder_child import Child + + object = self.parent_by_type(Child).object + return object.id + + @docs() + def ref_docs(self): + return get_docs_section("Syntax ExtResponse") + + @docs("response_id") + def response_id_docs(self): + if enum := self.root.gir.get_type("ResponseType", "Gtk"): + if member := enum.members.get(self.response_id, None): + return member.doc + + +def decompile_response_type(parent_element, child_element): + obj_id = None + for obj in child_element.children: + if obj.tag == "object": + obj_id = obj["id"] + break + + if obj_id is None: + return None + + for child in parent_element.children: + if child.tag == "action-widgets": + for action_widget in child.children: + if action_widget.cdata == obj_id: + response_id = action_widget["response"] + is_default = ( + " default" if decompile.truthy(action_widget["default"]) else "" + ) + return f"[action response={response_id}{is_default}]" + + return None + + +@decompiler("action-widgets", skip_children=True) +def decompile_action_widgets(ctx, gir): + # This is handled in the decompiler and decompile_response_type above + pass diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/translation_domain.py b/gtk/blueprint-compiler/blueprintcompiler/language/translation_domain.py new file mode 100644 index 00000000000..0f60af904b9 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/translation_domain.py @@ -0,0 +1,35 @@ +# translation_domain.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +from .common import * + + +class TranslationDomain(AstNode): + grammar = Statement( + "translation-domain", + UseQuoted("domain"), + ) + + @property + def domain(self): + return self.tokens["domain"] + + @docs() + def ref_docs(self): + return get_docs_section("Syntax TranslationDomain") diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/types.py b/gtk/blueprint-compiler/blueprintcompiler/language/types.py new file mode 100644 index 00000000000..fe45c4d52a9 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/types.py @@ -0,0 +1,184 @@ +# types.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from ..gir import Class, ExternType, Interface +from .common import * + + +class TypeName(AstNode): + grammar = AnyOf( + [ + UseIdent("namespace"), + ".", + UseIdent("class_name"), + ], + [ + AnyOf("$", [".", UseLiteral("old_extern", True)]), + UseIdent("class_name"), + UseLiteral("extern", True), + ], + UseIdent("class_name"), + ) + + @validate() + def old_extern(self): + if self.tokens["old_extern"]: + raise UpgradeWarning( + "Use the '$' extern syntax introduced in blueprint 0.8.0", + actions=[CodeAction("Use '$' syntax", "$" + self.tokens["class_name"])], + ) + + @validate("class_name") + def type_exists(self): + if not self.tokens["extern"] and self.gir_ns is not None: + self.root.gir.validate_type( + self.tokens["class_name"], self.tokens["namespace"] + ) + + @validate("namespace") + def gir_ns_exists(self): + if not self.tokens["extern"]: + try: + self.root.gir.validate_ns(self.tokens["namespace"]) + except CompileError as e: + ns = self.tokens["namespace"] + e.actions = [ + self.root.import_code_action(n, version) + for n, version in gir.get_available_namespaces() + if n == ns + ] + raise e + + @validate() + def deprecated(self) -> None: + if self.gir_type and self.gir_type.deprecated: + hints = [] + if self.gir_type.deprecated_doc: + hints.append(self.gir_type.deprecated_doc) + raise DeprecatedWarning( + f"{self.gir_type.full_name} is deprecated", + hints=hints, + ) + + @property + def gir_ns(self) -> T.Optional[gir.Namespace]: + if not self.tokens["extern"]: + return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk") + return None + + @property + def gir_type(self) -> gir.GirType: + if self.tokens["class_name"] and not self.tokens["extern"]: + return self.root.gir.get_type( + self.tokens["class_name"], self.tokens["namespace"] + ) + + return gir.ExternType(self.tokens["class_name"]) + + @property + def glib_type_name(self) -> str: + if gir_type := self.gir_type: + return gir_type.glib_type_name + else: + return self.tokens["class_name"] + + @docs("namespace") + def namespace_docs(self): + if ns := self.root.gir.namespaces.get(self.tokens["namespace"]): + return ns.doc + + @docs("class_name") + def class_docs(self): + if self.gir_type: + return self.gir_type.doc + + @property + def as_string(self) -> str: + if self.tokens["extern"]: + return "$" + self.tokens["class_name"] + elif self.tokens["namespace"]: + return f"{self.tokens['namespace']}.{self.tokens['class_name']}" + else: + return self.tokens["class_name"] + + +class ClassName(TypeName): + @validate("namespace", "class_name") + def gir_class_exists(self): + if ( + self.gir_type is not None + and not isinstance(self.gir_type, ExternType) + and not isinstance(self.gir_type, Class) + ): + if isinstance(self.gir_type, Interface): + raise CompileError( + f"{self.gir_type.full_name} is an interface, not a class" + ) + else: + raise CompileError(f"{self.gir_type.full_name} is not a class") + + +class ConcreteClassName(ClassName): + @validate("namespace", "class_name") + def not_abstract(self): + if isinstance(self.gir_type, Class) and self.gir_type.abstract: + raise CompileError( + f"{self.gir_type.full_name} can't be instantiated because it's abstract", + hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"], + ) + + +class TemplateClassName(ClassName): + """Handles the special case of a template type. The old syntax uses an identifier, + which is ambiguous with the new syntax. So this class displays an appropriate + upgrade warning instead of a class not found error.""" + + @property + def is_legacy(self): + return ( + self.tokens["extern"] is None + and self.tokens["namespace"] is None + and self.root.gir.get_type(self.tokens["class_name"], "Gtk") is None + ) + + @property + def gir_type(self) -> gir.GirType: + if self.is_legacy: + return gir.ExternType(self.tokens["class_name"]) + else: + return super().gir_type + + @validate("class_name") + def type_exists(self): + if self.is_legacy: + if type := self.root.gir.get_type_by_cname(self.tokens["class_name"]): + replacement = type.full_name + else: + replacement = "$" + self.tokens["class_name"] + + raise UpgradeWarning( + "Use type syntax here (introduced in blueprint 0.8.0)", + actions=[CodeAction("Use type syntax", replace_with=replacement)], + ) + + if not self.tokens["extern"] and self.gir_ns is not None: + self.root.gir.validate_type( + self.tokens["class_name"], self.tokens["namespace"] + ) diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/ui.py b/gtk/blueprint-compiler/blueprintcompiler/language/ui.py new file mode 100644 index 00000000000..d55a22a073c --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/ui.py @@ -0,0 +1,154 @@ +# ui.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +from functools import cached_property + +from .. import gir +from .common import * +from .contexts import ScopeCtx +from .gobject_object import Object +from .gtk_menu import Menu, menu +from .gtkbuilder_template import Template +from .imports import GtkDirective, Import +from .translation_domain import TranslationDomain +from .types import TypeName + + +class UI(AstNode): + """The AST node for the entire file""" + + grammar = [ + GtkDirective, + ZeroOrMore(Import), + Optional(TranslationDomain), + Until( + AnyOf( + Template, + menu, + Object, + ), + Eof(), + ), + ] + + @cached_property + def gir(self) -> gir.GirContext: + gir_ctx = gir.GirContext() + self._gir_errors = [] + + try: + if gtk := self.children[GtkDirective][0].gir_namespace: + gir_ctx.add_namespace(gtk) + except CompileError as e: + self._gir_errors.append(e) + + for i in self.children[Import]: + try: + if i.gir_namespace is not None: + gir_ctx.add_namespace(i.gir_namespace) + else: + gir_ctx.not_found_namespaces.add(i.namespace) + except CompileError as e: + e.range = i.range + self._gir_errors.append(e) + + return gir_ctx + + @property + def using(self) -> T.List[Import]: + return self.children[Import] + + @property + def gtk_decl(self) -> GtkDirective: + return self.children[GtkDirective][0] + + @property + def translation_domain(self) -> T.Optional[TranslationDomain]: + domains = self.children[TranslationDomain] + if len(domains): + return domains[0] + else: + return None + + @property + def contents(self) -> T.List[T.Union[Object, Template, Menu]]: + return [ + child + for child in self.children + if isinstance(child, Object) + or isinstance(child, Template) + or isinstance(child, Menu) + ] + + @property + def template(self) -> T.Optional[Template]: + if len(self.children[Template]): + return self.children[Template][0] + else: + return None + + def is_legacy_template(self, id: str) -> bool: + return ( + id not in self.context[ScopeCtx].objects + and self.template is not None + and self.template.class_name.glib_type_name == id + ) + + def import_code_action(self, ns: str, version: str) -> CodeAction: + if len(self.children[Import]): + pos = self.children[Import][-1].range.end + else: + pos = self.children[GtkDirective][0].range.end + + return CodeAction( + f"Import {ns} {version}", + f"\nusing {ns} {version};", + Range(pos, pos, self.group.text), + ) + + @cached_property + def used_imports(self) -> T.Optional[T.Set[str]]: + def _iter_recursive(node: AstNode): + yield node + for child in node.children: + if isinstance(child, AstNode): + yield from _iter_recursive(child) + + result = set() + for node in _iter_recursive(self): + if isinstance(node, TypeName): + ns = node.gir_ns + if ns is not None: + result.add(ns.name) + return result + + @context(ScopeCtx) + def scope_ctx(self) -> ScopeCtx: + return ScopeCtx(node=self) + + @validate() + def gir_errors(self): + # make sure gir is loaded + self.gir + if len(self._gir_errors): + raise MultipleErrors(self._gir_errors) + + @validate() + def unique_ids(self): + self.context[ScopeCtx].validate_unique_ids() diff --git a/gtk/blueprint-compiler/blueprintcompiler/language/values.py b/gtk/blueprint-compiler/blueprintcompiler/language/values.py new file mode 100644 index 00000000000..63cf4fcc0f0 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/language/values.py @@ -0,0 +1,492 @@ +# values.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import typing as T + +from blueprintcompiler.gir import ArrayType +from blueprintcompiler.lsp_utils import SemanticToken + +from .common import * +from .contexts import ScopeCtx, ValueTypeCtx +from .gobject_object import Object +from .types import TypeName + + +class Translated(AstNode): + grammar = AnyOf( + ["_", "(", UseQuoted("string"), ")"], + [ + "C_", + "(", + UseQuoted("context"), + ",", + UseQuoted("string"), + ")", + ], + ) + + @property + def string(self) -> str: + return self.tokens["string"] + + @property + def translate_context(self) -> T.Optional[str]: + return self.tokens["context"] + + @validate() + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if expected_type is not None and not expected_type.assignable_to(StringType()): + raise CompileError( + f"Cannot convert translated string to {expected_type.full_name}" + ) + + @docs() + def ref_docs(self): + return get_docs_section("Syntax Translated") + + +class TypeLiteral(AstNode): + grammar = [ + "typeof", + AnyOf( + [ + "<", + to_parse_node(TypeName).expected("type name"), + Match(">").expected(), + ], + [ + UseExact("lparen", "("), + to_parse_node(TypeName).expected("type name"), + UseExact("rparen", ")").expected("')'"), + ], + ), + ] + + @property + def type(self): + return gir.TypeType() + + @property + def type_name(self) -> TypeName: + return self.children[TypeName][0] + + @validate() + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if expected_type is not None and not isinstance(expected_type, gir.TypeType): + raise CompileError(f"Cannot convert GType to {expected_type.full_name}") + + @validate("lparen", "rparen") + def upgrade_to_angle_brackets(self): + if self.tokens["lparen"]: + raise UpgradeWarning( + "Use angle bracket syntax introduced in blueprint 0.8.0", + actions=[ + CodeAction( + "Use <> instead of ()", + f"<{self.children[TypeName][0].as_string}>", + ) + ], + ) + + @docs() + def ref_docs(self): + return get_docs_section("Syntax TypeLiteral") + + +class QuotedLiteral(AstNode): + grammar = UseQuoted("value") + + @property + def value(self) -> str: + return self.tokens["value"] + + @property + def type(self): + return gir.StringType() + + @validate() + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if ( + isinstance(expected_type, gir.IntType) + or isinstance(expected_type, gir.UIntType) + or isinstance(expected_type, gir.FloatType) + ): + raise CompileError(f"Cannot convert string to number") + + elif isinstance(expected_type, gir.StringType): + pass + + elif ( + isinstance(expected_type, gir.Class) + or isinstance(expected_type, gir.Interface) + or isinstance(expected_type, gir.Boxed) + ): + parseable_types = [ + "Gdk.Paintable", + "Gdk.Texture", + "Gdk.Pixbuf", + "Gio.File", + "Gtk.ShortcutTrigger", + "Gtk.ShortcutAction", + "Gdk.RGBA", + "Gdk.ContentFormats", + "Gsk.Transform", + "GLib.Variant", + ] + if expected_type.full_name not in parseable_types: + hints = [] + if isinstance(expected_type, gir.TypeType): + hints.append(f"use the typeof operator: 'typeof({self.value})'") + raise CompileError( + f"Cannot convert string to {expected_type.full_name}", hints=hints + ) + + elif expected_type is not None: + raise CompileError(f"Cannot convert string to {expected_type.full_name}") + + +class NumberLiteral(AstNode): + grammar = [ + Optional(AnyOf(UseExact("sign", "-"), UseExact("sign", "+"))), + UseNumber("value"), + ] + + @property + def type(self) -> gir.GirType: + if isinstance(self.value, int): + return gir.IntType() + else: + return gir.FloatType() + + @property + def value(self) -> T.Union[int, float]: + if self.tokens["sign"] == "-": + return -self.tokens["value"] + else: + return self.tokens["value"] + + @validate() + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if isinstance(expected_type, gir.IntType): + if not isinstance(self.value, int): + raise CompileError( + f"Cannot convert {self.group.tokens['value']} to integer" + ) + + elif isinstance(expected_type, gir.UIntType): + if self.value < 0: + raise CompileError( + f"Cannot convert -{self.group.tokens['value']} to unsigned integer" + ) + + elif not isinstance(expected_type, gir.FloatType) and expected_type is not None: + raise CompileError(f"Cannot convert number to {expected_type.full_name}") + + +class Flag(AstNode): + grammar = UseIdent("value") + + @property + def name(self) -> str: + return self.tokens["value"] + + @property + def value(self) -> T.Optional[int]: + type = self.context[ValueTypeCtx].value_type + if not isinstance(type, Enumeration): + return None + elif member := type.members.get(self.name): + return member.value + else: + return None + + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: + yield SemanticToken( + self.group.tokens["value"].start, + self.group.tokens["value"].end, + SemanticTokenType.EnumMember, + ) + + @docs() + def docs(self): + type = self.context[ValueTypeCtx].value_type + if not isinstance(type, Enumeration): + return + if member := type.members.get(self.tokens["value"]): + return member.doc + + @validate() + def validate_for_type(self): + expected_type = self.context[ValueTypeCtx].value_type + if ( + isinstance(expected_type, gir.Bitfield) + and self.tokens["value"] not in expected_type.members + ): + raise CompileError( + f"{self.tokens['value']} is not a member of {expected_type.full_name}", + did_you_mean=(self.tokens["value"], expected_type.members.keys()), + ) + + @validate() + def unique(self): + self.validate_unique_in_parent( + f"Duplicate flag '{self.name}'", lambda x: x.name == self.name + ) + + +class Flags(AstNode): + grammar = [Flag, "|", Flag, ZeroOrMore(["|", Flag])] + + @property + def flags(self) -> T.List[Flag]: + return self.children + + @validate() + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if expected_type is not None and not isinstance(expected_type, gir.Bitfield): + raise CompileError(f"{expected_type.full_name} is not a bitfield type") + + @docs() + def ref_docs(self): + return get_docs_section("Syntax Flags") + + +class IdentLiteral(AstNode): + grammar = UseIdent("value") + + @property + def ident(self) -> str: + return self.tokens["value"] + + @property + def type(self) -> T.Optional[gir.GirType]: + # If the expected type is known, then use that. Otherwise, guess. + if expected_type := self.context[ValueTypeCtx].value_type: + return expected_type + elif self.ident in ["true", "false"]: + return gir.BoolType() + elif object := self.context[ScopeCtx].objects.get(self.ident): + return object.gir_class + elif self.root.is_legacy_template(self.ident): + return self.root.template.class_name.gir_type + else: + return None + + @validate() + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if isinstance(expected_type, gir.BoolType): + if self.ident not in ["true", "false"]: + raise CompileError(f"Expected 'true' or 'false' for boolean value") + + elif isinstance(expected_type, gir.Enumeration): + if self.ident not in expected_type.members: + raise CompileError( + f"{self.ident} is not a member of {expected_type.full_name}", + did_you_mean=(self.ident, list(expected_type.members.keys())), + ) + + elif self.root.is_legacy_template(self.ident): + raise UpgradeWarning( + "Use 'template' instead of the class name (introduced in 0.8.0)", + actions=[CodeAction("Use 'template'", "template")], + ) + + elif expected_type is not None or self.context[ValueTypeCtx].must_infer_type: + object = self.context[ScopeCtx].objects.get(self.ident) + if object is None: + if self.ident == "null": + if not self.context[ValueTypeCtx].allow_null: + raise CompileError("null is not permitted here") + else: + raise CompileError( + f"Could not find object with ID {self.ident}", + did_you_mean=( + self.ident, + self.context[ScopeCtx].objects.keys(), + ), + ) + elif ( + expected_type is not None + and object.gir_class is not None + and not object.gir_class.assignable_to(expected_type) + ): + raise CompileError( + f"Cannot assign {object.gir_class.full_name} to {expected_type.full_name}" + ) + + @docs() + def docs(self) -> T.Optional[str]: + expected_type = self.context[ValueTypeCtx].value_type + if isinstance(expected_type, gir.BoolType): + return None + elif isinstance(expected_type, gir.Enumeration): + if member := expected_type.members.get(self.ident): + return member.doc + else: + return expected_type.doc + elif self.ident == "null" and self.context[ValueTypeCtx].allow_null: + return None + elif object := self.context[ScopeCtx].objects.get(self.ident): + return f"```\n{object.signature}\n```" + elif self.root.is_legacy_template(self.ident): + return f"```\n{self.root.template.signature}\n```" + else: + return None + + def get_semantic_tokens(self) -> T.Iterator[SemanticToken]: + type = self.context[ValueTypeCtx].value_type + if isinstance(type, gir.Enumeration): + token = self.group.tokens["value"] + yield SemanticToken(token.start, token.end, SemanticTokenType.EnumMember) + + def get_reference(self, _idx: int) -> T.Optional[LocationLink]: + ref = self.context[ScopeCtx].objects.get(self.ident) + if ref is None and self.root.is_legacy_template(self.ident): + ref = self.root.template + + if ref: + return LocationLink(self.range, ref.range, ref.ranges["id"]) + else: + return None + + +class Literal(AstNode): + grammar = AnyOf( + TypeLiteral, + QuotedLiteral, + NumberLiteral, + IdentLiteral, + ) + + @property + def value( + self, + ) -> T.Union[TypeLiteral, QuotedLiteral, NumberLiteral, IdentLiteral]: + return self.children[0] + + +class ObjectValue(AstNode): + grammar = Object + + @property + def object(self) -> Object: + return self.children[Object][0] + + @validate() + def validate_for_type(self) -> None: + expected_type = self.context[ValueTypeCtx].value_type + if ( + expected_type is not None + and self.object.gir_class is not None + and not self.object.gir_class.assignable_to(expected_type) + ): + raise CompileError( + f"Cannot assign {self.object.gir_class.full_name} to {expected_type.full_name}" + ) + + +class Value(AstNode): + grammar = AnyOf(Translated, Flags, Literal) + + @property + def child( + self, + ) -> T.Union[Translated, Flags, Literal]: + return self.children[0] + + +class ArrayValue(AstNode): + grammar = ["[", Delimited(Value, ","), "]"] + + @validate() + def validate_for_type(self) -> None: + expected_type = self.gir_type + if expected_type is not None and not isinstance(expected_type, gir.ArrayType): + raise CompileError(f"Cannot assign array to {expected_type.full_name}") + + if expected_type is not None and not isinstance( + expected_type.inner, StringType + ): + raise CompileError("Only string arrays are supported") + + @validate() + def validate_invalid_newline(self) -> None: + expected_type = self.gir_type + if isinstance(expected_type, gir.ArrayType) and isinstance( + expected_type.inner, StringType + ): + errors = [] + for value in self.values: + if isinstance(value.child, Literal) and isinstance( + value.child.value, QuotedLiteral + ): + quoted_literal = value.child.value + literal_value = quoted_literal.value + # literal_value can be None if there's an invalid escape sequence + if literal_value is not None and "\n" in literal_value: + errors.append( + CompileError( + "String literals inside arrays can't contain newlines", + range=quoted_literal.range, + ) + ) + if len(errors) > 0: + raise MultipleErrors(errors) + + @property + def values(self) -> T.List[Value]: + return self.children + + @property + def gir_type(self): + return self.parent.context[ValueTypeCtx].value_type + + @context(ValueTypeCtx) + def child_value(self): + if self.gir_type is None or not isinstance(self.gir_type, ArrayType): + return ValueTypeCtx(None) + else: + return ValueTypeCtx(self.gir_type.inner) + + +class StringValue(AstNode): + grammar = AnyOf(Translated, QuotedLiteral) + + @property + def child( + self, + ) -> T.Union[Translated, QuotedLiteral]: + return self.children[0] + + @property + def string(self) -> str: + if isinstance(self.child, Translated): + return self.child.string + else: + return self.child.value + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(StringType()) diff --git a/gtk/blueprint-compiler/blueprintcompiler/lsp.py b/gtk/blueprint-compiler/blueprintcompiler/lsp.py new file mode 100644 index 00000000000..06591549320 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/lsp.py @@ -0,0 +1,499 @@ +# lsp.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +import json +import sys +import traceback +import typing as T +from difflib import SequenceMatcher + +from . import decompiler, formatter, parser, tokenizer, utils, xml_reader +from .ast_utils import AstNode +from .completions import complete +from .errors import CompileError, MultipleErrors +from .lsp_utils import * +from .outputs.xml import XmlOutput +from .tokenizer import Token + + +def printerr(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + +def command(json_method: str): + def decorator(func): + func._json_method = json_method + return func + + return decorator + + +class OpenFile: + def __init__(self, uri: str, text: str, version: int) -> None: + self.uri = uri + self.text = text + self.version = version + self.ast: T.Optional[AstNode] = None + self.tokens: T.Optional[list[Token]] = None + + self._update() + + def apply_changes(self, changes) -> None: + for change in changes: + if "range" not in change: + self.text = change["text"] + continue + start = utils.pos_to_idx( + change["range"]["start"]["line"], + change["range"]["start"]["character"], + self.text, + ) + end = utils.pos_to_idx( + change["range"]["end"]["line"], + change["range"]["end"]["character"], + self.text, + ) + self.text = self.text[:start] + change["text"] + self.text[end:] + self._update() + + def _update(self) -> None: + self.diagnostics: list[CompileError] = [] + try: + self.tokens = tokenizer.tokenize(self.text) + self.ast, errors, warnings = parser.parse(self.tokens) + self.diagnostics += warnings + if errors is not None: + self.diagnostics += errors.errors + except MultipleErrors as e: + self.diagnostics += e.errors + except CompileError as e: + self.diagnostics.append(e) + + def calc_semantic_tokens(self) -> T.List[int]: + if self.ast is None: + return [] + + tokens = list(self.ast.get_semantic_tokens()) + token_lists = [ + [ + *utils.idx_to_pos(token.start, self.text), # line and column + token.end - token.start, # length + token.type, + 0, # token modifiers + ] + for token in tokens + ] + + # convert line, column numbers to deltas + for a, b in zip(token_lists[-2::-1], token_lists[:0:-1]): + b[0] -= a[0] + if b[0] == 0: + b[1] -= a[1] + + # flatten the list + return [x for y in token_lists for x in y] + + +class LanguageServer: + commands: T.Dict[str, T.Callable] = {} + + def __init__(self): + self.client_capabilities = {} + self.client_supports_completion_choice = False + self._open_files: T.Dict[str, OpenFile] = {} + + def run(self): + # Read tags from gir files. During normal compilation these are + # ignored. + xml_reader.PARSE_GIR.add("doc") + + try: + while True: + line = "" + content_len = -1 + while content_len == -1 or (line != "\n" and line != "\r\n"): + line = sys.stdin.buffer.readline().decode() + if line == "": + return + if line.startswith("Content-Length:"): + content_len = int(line.split("Content-Length:")[1].strip()) + line = sys.stdin.buffer.read(content_len).decode() + printerr("input: " + line) + + data = json.loads(line) + method = data.get("method") + id = data.get("id") + params = data.get("params") + + if method in self.commands: + self.commands[method](self, id, params) + except Exception as e: + printerr(traceback.format_exc()) + + def _send(self, data): + data["jsonrpc"] = "2.0" + line = json.dumps(data, separators=(",", ":")) + printerr("output: " + line) + sys.stdout.write( + f"Content-Length: {len(line.encode())}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{line}" + ) + sys.stdout.flush() + + def _send_error(self, id, code, message, data=None): + self._send( + { + "id": id, + "error": { + "code": code, + "message": message, + "data": data, + }, + } + ) + + def _send_response(self, id, result): + self._send( + { + "id": id, + "result": result, + } + ) + + def _send_notification(self, method, params): + self._send( + { + "method": method, + "params": params, + } + ) + + @command("initialize") + def initialize(self, id, params): + from . import main + + self.client_capabilities = params.get("capabilities", {}) + self.client_supports_completion_choice = params.get("clientInfo", {}).get( + "name" + ) in ["Visual Studio Code", "VSCodium"] + self._send_response( + id, + { + "capabilities": { + "textDocumentSync": { + "openClose": True, + "change": TextDocumentSyncKind.Incremental, + }, + "semanticTokensProvider": { + "legend": { + "tokenTypes": ["enumMember"], + "tokenModifiers": [], + }, + "full": True, + }, + "completionProvider": {}, + "codeActionProvider": {}, + "hoverProvider": True, + "documentSymbolProvider": True, + "definitionProvider": True, + "documentFormattingProvider": True, + }, + "serverInfo": { + "name": "Blueprint", + "version": main.VERSION, + }, + }, + ) + + @command("textDocument/didOpen") + def didOpen(self, id, params): + doc = params.get("textDocument") + uri = doc.get("uri") + version = doc.get("version") + text = doc.get("text") + + open_file = OpenFile(uri, text, version) + self._open_files[uri] = open_file + self._send_file_updates(open_file) + + @command("textDocument/didChange") + def didChange(self, id, params): + if params is not None: + open_file = self._open_files[params["textDocument"]["uri"]] + open_file.apply_changes(params["contentChanges"]) + self._send_file_updates(open_file) + + @command("textDocument/didClose") + def didClose(self, id, params): + del self._open_files[params["textDocument"]["uri"]] + + @command("textDocument/hover") + def hover(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + docs = open_file.ast and open_file.ast.get_docs( + utils.pos_to_idx( + params["position"]["line"], + params["position"]["character"], + open_file.text, + ) + ) + if docs: + self._send_response( + id, + { + "contents": { + "kind": "markdown", + "value": docs, + } + }, + ) + else: + self._send_response(id, None) + + @command("textDocument/completion") + def completion(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + + if open_file.ast is None: + self._send_response(id, []) + return + + idx = utils.pos_to_idx( + params["position"]["line"], params["position"]["character"], open_file.text + ) + completions = complete(self, open_file.ast, open_file.tokens, idx) + self._send_response( + id, [completion.to_json(True) for completion in completions] + ) + + @command("textDocument/formatting") + def formatting(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + + if open_file.text is None: + self._send_error(id, ErrorCode.RequestFailed, "Document is not open") + return + + try: + formatted_blp = formatter.format( + open_file.text, + params["options"]["tabSize"], + params["options"]["insertSpaces"], + ) + except PrintableError: + self._send_error(id, ErrorCode.RequestFailed, "Could not format document") + return + + lst = [] + for tag, i1, i2, j1, j2 in SequenceMatcher( + None, open_file.text, formatted_blp + ).get_opcodes(): + if tag in ("replace", "insert", "delete"): + lst.append( + TextEdit( + Range(i1, i2, open_file.text), + "" if tag == "delete" else formatted_blp[j1:j2], + ).to_json() + ) + + self._send_response(id, lst) + + @command("textDocument/x-blueprint-compile") + def compile(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + + if open_file.ast is None: + self._send_error(id, ErrorCode.RequestFailed, "Document is not open") + return + + xml = None + try: + output = XmlOutput() + xml = output.emit(open_file.ast, indent=2, generated_notice=False) + except: + printerr(traceback.format_exc()) + self._send_error(id, ErrorCode.RequestFailed, "Could not compile document") + return + self._send_response(id, {"xml": xml}) + + @command("x-blueprint/decompile") + def decompile(self, id, params): + text = params.get("text") + blp = None + if text.strip() == "": + blp = "" + printerr("Decompiled to empty blueprint because input was empty") + else: + try: + blp = decompiler.decompile_string(text) + except decompiler.UnsupportedError as e: + self._send_error(id, ErrorCode.RequestFailed, e.message) + return + except: + printerr(traceback.format_exc()) + self._send_error(id, ErrorCode.RequestFailed, "Invalid input") + return + + self._send_response(id, {"blp": blp}) + + @command("textDocument/semanticTokens/full") + def semantic_tokens(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + + self._send_response( + id, + { + "data": open_file.calc_semantic_tokens(), + }, + ) + + @command("textDocument/codeAction") + def code_actions(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + + range = Range( + utils.pos_to_idx( + params["range"]["start"]["line"], + params["range"]["start"]["character"], + open_file.text, + ), + utils.pos_to_idx( + params["range"]["end"]["line"], + params["range"]["end"]["character"], + open_file.text, + ), + open_file.text, + ) + + actions = [ + { + "title": action.title, + "kind": "quickfix", + "diagnostics": [self._create_diagnostic(open_file.uri, diagnostic)], + "edit": { + "changes": { + open_file.uri: [ + { + "range": ( + action.edit_range.to_json() + if action.edit_range + else diagnostic.range.to_json() + ), + "newText": action.replace_with, + } + ] + } + }, + } + for diagnostic in open_file.diagnostics + if range.overlaps(diagnostic.range) + for action in diagnostic.actions + ] + + self._send_response(id, actions) + + @command("textDocument/documentSymbol") + def document_symbols(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + symbols = open_file.ast.get_document_symbols() + + def to_json(symbol: DocumentSymbol): + result = { + "name": symbol.name, + "kind": symbol.kind, + "range": symbol.range.to_json(), + "selectionRange": symbol.selection_range.to_json(), + "children": [to_json(child) for child in symbol.children], + } + if symbol.detail is not None: + result["detail"] = symbol.detail + return result + + self._send_response(id, [to_json(symbol) for symbol in symbols]) + + @command("textDocument/definition") + def definition(self, id, params): + open_file = self._open_files[params["textDocument"]["uri"]] + idx = utils.pos_to_idx( + params["position"]["line"], params["position"]["character"], open_file.text + ) + definition = open_file.ast.get_reference(idx) + if definition is None: + self._send_response(id, None) + else: + self._send_response( + id, + definition.to_json(open_file.uri), + ) + + def _send_file_updates(self, open_file: OpenFile): + self._send_notification( + "textDocument/publishDiagnostics", + { + "uri": open_file.uri, + "diagnostics": [ + self._create_diagnostic(open_file.uri, err) + for err in open_file.diagnostics + ], + }, + ) + + def _create_diagnostic(self, uri: str, err: CompileError): + message = err.message + + assert err.range is not None + + for hint in err.hints: + message += "\nhint: " + hint + + result = { + "range": err.range.to_json(), + "message": message, + "severity": ( + DiagnosticSeverity.Warning + if isinstance(err, CompileWarning) + else DiagnosticSeverity.Error + ), + } + + if isinstance(err, DeprecatedWarning): + result["tags"] = [DiagnosticTag.Deprecated] + + if isinstance(err, UnusedWarning): + result["tags"] = [DiagnosticTag.Unnecessary] + + if len(err.references) > 0: + result["relatedInformation"] = [ + { + "location": { + "uri": uri, + "range": ref.range.to_json(), + }, + "message": ref.message, + } + for ref in err.references + ] + + return result + + +for name in dir(LanguageServer): + item = getattr(LanguageServer, name) + if callable(item) and hasattr(item, "_json_method"): + LanguageServer.commands[item._json_method] = item diff --git a/gtk/blueprint-compiler/blueprintcompiler/lsp_utils.py b/gtk/blueprint-compiler/blueprintcompiler/lsp_utils.py new file mode 100644 index 00000000000..b9381813efc --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/lsp_utils.py @@ -0,0 +1,228 @@ +# lsp_enums.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +import enum +import json +import os +import typing as T +from dataclasses import dataclass, field + +from .errors import * +from .tokenizer import Range +from .utils import * + + +class TextDocumentSyncKind(enum.IntEnum): + None_ = 0 + Full = 1 + Incremental = 2 + + +class CompletionItemTag(enum.IntEnum): + Deprecated = 1 + + +class InsertTextFormat(enum.IntEnum): + PlainText = 1 + Snippet = 2 + + +class CompletionItemKind(enum.IntEnum): + Text = 1 + Method = 2 + Function = 3 + Constructor = 4 + Field = 5 + Variable = 6 + Class = 7 + Interface = 8 + Module = 9 + Property = 10 + Unit = 11 + Value = 12 + Enum = 13 + Keyword = 14 + Snippet = 15 + Color = 16 + File = 17 + Reference = 18 + Folder = 19 + EnumMember = 20 + Constant = 21 + Struct = 22 + Event = 23 + Operator = 24 + TypeParameter = 25 + + +class ErrorCode(enum.IntEnum): + RequestFailed = -32803 + + +@dataclass +class Completion: + label: str + kind: CompletionItemKind + signature: T.Optional[str] = None + deprecated: bool = False + sort_text: T.Optional[str] = None + docs: T.Optional[str] = None + text: T.Optional[str] = None + snippet: T.Optional[str] = None + detail: T.Optional[str] = None + + def to_json(self, snippets: bool): + insert_text = self.text or self.label + insert_text_format = InsertTextFormat.PlainText + if snippets and self.snippet: + insert_text = self.snippet + insert_text_format = InsertTextFormat.Snippet + + result = { + "label": self.label, + "kind": self.kind, + "tags": [CompletionItemTag.Deprecated] if self.deprecated else None, + # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemLabelDetails + "labelDetails": ({"detail": self.signature} if self.signature else None), + "documentation": ( + { + "kind": "markdown", + "value": self.docs, + } + if self.docs + else None + ), + "deprecated": self.deprecated, + "sortText": self.sort_text, + "insertText": insert_text, + "insertTextFormat": insert_text_format, + "detail": self.detail if self.detail else None, + } + return {k: v for k, v in result.items() if v is not None} + + +class SemanticTokenType(enum.IntEnum): + EnumMember = 0 + + +class DiagnosticSeverity(enum.IntEnum): + Error = 1 + Warning = 2 + Information = 3 + Hint = 4 + + +class DiagnosticTag(enum.IntEnum): + Unnecessary = 1 + Deprecated = 2 + + +@dataclass +class SemanticToken: + start: int + end: int + type: SemanticTokenType + + +class SymbolKind(enum.IntEnum): + File = 1 + Module = 2 + Namespace = 3 + Package = 4 + Class = 5 + Method = 6 + Property = 7 + Field = 8 + Constructor = 9 + Enum = 10 + Interface = 11 + Function = 12 + Variable = 13 + Constant = 14 + String = 15 + Number = 16 + Boolean = 17 + Array = 18 + Object = 19 + Key = 20 + Null = 21 + EnumMember = 22 + Struct = 23 + Event = 24 + Operator = 25 + TypeParameter = 26 + + +@dataclass +class DocumentSymbol: + name: str + kind: SymbolKind + range: Range + selection_range: Range + detail: T.Optional[str] = None + children: T.List["DocumentSymbol"] = field(default_factory=list) + + +@dataclass +class LocationLink: + origin_selection_range: Range + target_range: Range + target_selection_range: Range + + def to_json(self, target_uri: str): + return { + "originSelectionRange": self.origin_selection_range.to_json(), + "targetUri": target_uri, + "targetRange": self.target_range.to_json(), + "targetSelectionRange": self.target_selection_range.to_json(), + } + + +@dataclass +class TextEdit: + range: Range + newText: str + + def to_json(self): + return {"range": self.range.to_json(), "newText": self.newText} + + +_docs_sections: T.Optional[dict[str, T.Any]] = None + + +def get_docs_section(section_name: str) -> T.Optional[str]: + global _docs_sections + + if _docs_sections is None: + try: + with open( + os.path.join(os.path.dirname(__file__), "reference_docs.json") + ) as f: + _docs_sections = json.load(f) + except FileNotFoundError: + _docs_sections = {} + + if section := _docs_sections.get(section_name): + content = section["content"] + link = section["link"] + content += f"\n\n---\n\n[Online documentation]({link})" + return content + else: + return None diff --git a/gtk/blueprint-compiler/blueprintcompiler/main.py b/gtk/blueprint-compiler/blueprintcompiler/main.py new file mode 100644 index 00000000000..1c3a1c60ffe --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/main.py @@ -0,0 +1,355 @@ +# main.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +import argparse +import difflib +import os +import sys +import typing as T + +from . import formatter, interactive_port, parser, tokenizer +from .decompiler import decompile_string +from .errors import CompileError, CompilerBugError, PrintableError, report_bug +from .gir import add_typelib_search_path +from .lsp import LanguageServer +from .outputs import XmlOutput +from .utils import Colors + +VERSION = "uninstalled" +LIBDIR = None + + +class BlueprintApp: + def main(self): + self.parser = argparse.ArgumentParser() + self.subparsers = self.parser.add_subparsers(metavar="command") + self.parser.set_defaults(func=self.cmd_help) + + compile = self.add_subcommand( + "compile", "Compile blueprint files", self.cmd_compile + ) + compile.add_argument("--output", dest="output", default="-") + compile.add_argument("--typelib-path", nargs="?", action="append") + compile.add_argument( + "input", metavar="filename", default=sys.stdin, type=argparse.FileType("r") + ) + + batch_compile = self.add_subcommand( + "batch-compile", + "Compile many blueprint files at once", + self.cmd_batch_compile, + ) + batch_compile.add_argument("output_dir", metavar="output-dir") + batch_compile.add_argument("input_dir", metavar="input-dir") + batch_compile.add_argument("--typelib-path", nargs="?", action="append") + batch_compile.add_argument( + "inputs", + nargs="+", + metavar="filenames", + default=sys.stdin, + type=argparse.FileType("r"), + ) + + format = self.add_subcommand( + "format", "Format given blueprint files", self.cmd_format + ) + format.add_argument( + "-f", + "--fix", + help="Apply the edits to the files", + default=False, + action="store_true", + ) + format.add_argument( + "-t", + "--tabs", + help="Use tabs instead of spaces", + default=False, + action="store_true", + ) + format.add_argument( + "-s", + "--spaces-num", + help="How many spaces should be used per indent", + default=2, + type=int, + ) + format.add_argument( + "-n", + "--no-diff", + help="Do not print a full diff of the changes", + default=False, + action="store_true", + ) + format.add_argument( + "inputs", + nargs="+", + metavar="filenames", + ) + + decompile = self.add_subcommand( + "decompile", "Convert .ui XML files to blueprint", self.cmd_decompile + ) + decompile.add_argument("--output", dest="output", default="-") + decompile.add_argument("--typelib-path", nargs="?", action="append") + decompile.add_argument( + "input", metavar="filename", default=sys.stdin, type=argparse.FileType("r") + ) + + port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port) + + lsp = self.add_subcommand( + "lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp + ) + + self.add_subcommand("help", "Show this message", self.cmd_help) + + self.parser.add_argument("--version", action="version", version=VERSION) + + try: + opts = self.parser.parse_args() + opts.func(opts) + except SystemExit as e: + raise e + except KeyboardInterrupt: + print(f"\n\n{Colors.RED}{Colors.BOLD}Interrupted.{Colors.CLEAR}") + except EOFError: + print(f"\n\n{Colors.RED}{Colors.BOLD}Interrupted.{Colors.CLEAR}") + except: + report_bug() + + def add_subcommand(self, name: str, help: str, func): + parser = self.subparsers.add_parser(name, help=help) + parser.set_defaults(func=func) + return parser + + def cmd_help(self, opts): + self.parser.print_help() + + def cmd_compile(self, opts): + if opts.typelib_path != None: + for typelib_path in opts.typelib_path: + add_typelib_search_path(typelib_path) + + data = opts.input.read() + try: + xml, warnings = self._compile(data) + + for warning in warnings: + warning.pretty_print(opts.input.name, data, stream=sys.stderr) + + if opts.output == "-": + print(xml) + else: + with open(opts.output, "w") as file: + file.write(xml) + except PrintableError as e: + e.pretty_print(opts.input.name, data, stream=sys.stderr) + sys.exit(1) + + def cmd_batch_compile(self, opts): + if opts.typelib_path != None: + for typelib_path in opts.typelib_path: + add_typelib_search_path(typelib_path) + + for file in opts.inputs: + data = file.read() + file_abs = os.path.abspath(file.name) + input_dir_abs = os.path.abspath(opts.input_dir) + + try: + if not os.path.commonpath([file_abs, input_dir_abs]): + print( + f"{Colors.RED}{Colors.BOLD}error: input file '{file.name}' is not in input directory '{opts.input_dir}'{Colors.CLEAR}" + ) + sys.exit(1) + + xml, warnings = self._compile(data) + + for warning in warnings: + warning.pretty_print(file.name, data, stream=sys.stderr) + + path = os.path.join( + opts.output_dir, + os.path.relpath( + os.path.splitext(file.name)[0] + ".ui", opts.input_dir + ), + ) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as file: + file.write(xml) + except PrintableError as e: + e.pretty_print(file.name, data) + sys.exit(1) + + def cmd_format(self, opts): + input_files = [] + missing_files = [] + panic = False + formatted_files = 0 + skipped_files = 0 + + for path in opts.inputs: + if os.path.isfile(path): + input_files.append(path) + elif os.path.isdir(path): + for root, subfolders, files in os.walk(path): + for file in files: + if file.endswith(".blp"): + input_files.append(os.path.join(root, file)) + else: + missing_files.append(path) + + for file in input_files: + with open(file, "r+") as file: + data = file.read() + errored = False + + try: + self._compile(data) + except: + errored = True + + formatted_str = formatter.format(data, opts.spaces_num, not opts.tabs) + + if data != formatted_str: + happened = "Would format" + + if opts.fix and not errored: + file.seek(0) + file.truncate() + file.write(formatted_str) + happened = "Formatted" + + if not opts.no_diff: + diff_lines = [] + a_lines = data.splitlines(keepends=True) + b_lines = formatted_str.splitlines(keepends=True) + + for line in difflib.unified_diff( + a_lines, b_lines, fromfile=file.name, tofile=file.name, n=5 + ): + # Work around https://bugs.python.org/issue2142 + # See: + # https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html + if line[-1] == "\n": + diff_lines.append(line) + else: + diff_lines.append(line + "\n") + diff_lines.append("\\ No newline at end of file\n") + + print("".join(diff_lines)) + + to_print = Colors.BOLD + if errored: + to_print += f"{Colors.RED}Skipped {file.name}: Will not overwrite file with compile errors" + panic = True + skipped_files += 1 + else: + to_print += f"{happened} {file.name}" + formatted_files += 1 + + print(to_print) + print(Colors.CLEAR) + + missing_num = len(missing_files) + summary = "" + + if missing_num > 0: + print( + f"{Colors.BOLD}{Colors.RED}Could not find files:{Colors.CLEAR}{Colors.BOLD}" + ) + for path in missing_files: + print(f" {path}") + print(Colors.CLEAR) + panic = True + + if len(input_files) == 0: + print(f"{Colors.RED}No Blueprint files found") + sys.exit(1) + + def would_be(verb): + return verb if opts.fix else f"would be {verb}" + + def how_many(count, bold=True): + string = f"{Colors.BLUE}{count} {'files' if count != 1 else 'file'}{Colors.CLEAR}" + return Colors.BOLD + string + Colors.BOLD if bold else Colors.CLEAR + string + + if formatted_files > 0: + summary += f"{how_many(formatted_files)} {would_be('formatted')}, " + panic = panic or not opts.fix + + left_files = len(input_files) - formatted_files - skipped_files + summary += f"{how_many(left_files, False)} {would_be('left unchanged')}" + + if skipped_files > 0: + summary += f", {how_many(skipped_files)} {would_be('skipped')}" + + if missing_num > 0: + summary += f", {how_many(missing_num)} not found" + + print(summary + Colors.CLEAR) + + if panic: + sys.exit(1) + + def cmd_decompile(self, opts): + if opts.typelib_path != None: + for typelib_path in opts.typelib_path: + add_typelib_search_path(typelib_path) + + data = opts.input.read() + try: + decompiled = decompile_string(data) + + if opts.output == "-": + print(decompiled) + else: + with open(opts.output, "w") as file: + file.write(decompiled) + except PrintableError as e: + e.pretty_print(opts.input.name, data, stream=sys.stderr) + sys.exit(1) + + def cmd_lsp(self, opts): + langserv = LanguageServer() + langserv.run() + + def cmd_port(self, opts): + interactive_port.run(opts) + + def _compile(self, data: str) -> T.Tuple[str, T.List[CompileError]]: + tokens = tokenizer.tokenize(data) + ast, errors, warnings = parser.parse(tokens) + + if errors: + raise errors + if ast is None: + raise CompilerBugError() + + formatter = XmlOutput() + + return formatter.emit(ast), warnings + + +def main(version, libdir): + global VERSION, LIBDIR + VERSION, LIBDIR = version, libdir + BlueprintApp().main() diff --git a/gtk/blueprint-compiler/blueprintcompiler/outputs/__init__.py b/gtk/blueprint-compiler/blueprintcompiler/outputs/__init__.py new file mode 100644 index 00000000000..6cdb07b1f8c --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/outputs/__init__.py @@ -0,0 +1,9 @@ +from ..language import UI + + +class OutputFormat: + def emit(self, ui: UI) -> str: + raise NotImplementedError() + + +from .xml import XmlOutput diff --git a/gtk/blueprint-compiler/blueprintcompiler/outputs/xml/__init__.py b/gtk/blueprint-compiler/blueprintcompiler/outputs/xml/__init__.py new file mode 100644 index 00000000000..5e438346805 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/outputs/xml/__init__.py @@ -0,0 +1,420 @@ +import typing as T + +from ...language import * +from .. import OutputFormat +from .xml_emitter import XmlEmitter + + +class XmlOutput(OutputFormat): + def emit(self, ui: UI, indent=2, generated_notice=True) -> str: + xml = XmlEmitter(indent, generated_notice) + self._emit_ui(ui, xml) + return xml.result + + def _emit_ui(self, ui: UI, xml: XmlEmitter): + if domain := ui.translation_domain: + xml.start_tag("interface", domain=domain.domain) + else: + xml.start_tag("interface") + + self._emit_gtk_directive(ui.gtk_decl, xml) + + for x in ui.contents: + if isinstance(x, Template): + self._emit_template(x, xml) + elif isinstance(x, Object): + self._emit_object(x, xml) + elif isinstance(x, Menu): + self._emit_menu(x, xml) + else: + raise CompilerBugError() + + xml.end_tag() + + def _emit_gtk_directive(self, gtk: GtkDirective, xml: XmlEmitter): + xml.put_self_closing("requires", lib="gtk", version=gtk.gir_namespace.version) + + def _emit_template(self, template: Template, xml: XmlEmitter): + xml.start_tag( + "template", **{"class": template.gir_class}, parent=template.parent_type + ) + self._emit_object_or_template(template, xml) + xml.end_tag() + + def _emit_object(self, obj: Object, xml: XmlEmitter): + xml.start_tag( + "object", + **{"class": obj.class_name}, + id=obj.id, + ) + self._emit_object_or_template(obj, xml) + xml.end_tag() + + def _emit_object_or_template( + self, obj: T.Union[Object, Template, ExtListItemFactory], xml: XmlEmitter + ): + for child in obj.content.children: + if isinstance(child, Property): + self._emit_property(child, xml) + elif isinstance(child, Signal): + self._emit_signal(child, xml) + elif isinstance(child, Child): + self._emit_child(child, xml) + else: + self._emit_extensions(child, xml) + + # List action widgets + action_widgets = obj.action_widgets + if action_widgets: + xml.start_tag("action-widgets") + for action_widget in action_widgets: + xml.start_tag( + "action-widget", + response=action_widget.response_id, + default=action_widget.is_default or None, + ) + xml.put_text(action_widget.widget_id) + xml.end_tag() + xml.end_tag() + + def _emit_menu(self, menu: Menu, xml: XmlEmitter): + xml.start_tag(menu.tag, id=menu.id) + for child in menu.items: + if isinstance(child, Menu): + self._emit_menu(child, xml) + elif isinstance(child, MenuAttribute): + xml.start_tag( + "attribute", + name=child.name, + **self._translated_string_attrs(child.value.child), + ) + xml.put_text(child.value.string) + xml.end_tag() + else: + raise CompilerBugError() + xml.end_tag() + + def _emit_property(self, property: Property, xml: XmlEmitter): + value = property.value + + props: T.Dict[str, T.Optional[str]] = { + "name": property.name, + } + + if isinstance(value, Value): + child = value.child + + if isinstance(child, Translated): + xml.start_tag( + "property", **props, **self._translated_string_attrs(child) + ) + xml.put_text(child.string) + xml.end_tag() + else: + xml.start_tag("property", **props) + self._emit_value(value, xml) + xml.end_tag() + + elif isinstance(value, Binding): + if simple := value.simple_binding: + props["bind-source"] = self._object_id(value, simple.source) + props["bind-property"] = simple.property_name + flags = [] + if not simple.no_sync_create: + flags.append("sync-create") + if simple.inverted: + flags.append("invert-boolean") + if simple.bidirectional: + flags.append("bidirectional") + props["bind-flags"] = "|".join(flags) or None + + xml.put_self_closing("property", **props) + else: + xml.start_tag("binding", **props) + self._emit_expression(value.expression, xml) + xml.end_tag() + + elif isinstance(value, ObjectValue): + xml.start_tag("property", **props) + self._emit_object(value.object, xml) + xml.end_tag() + + elif isinstance(value, ArrayValue): + xml.start_tag("property", **props) + values = list(value.values) + for value in values[:-1]: + self._emit_value(value, xml) + xml.put_text("\n") + self._emit_value(values[-1], xml) + xml.end_tag() + + else: + raise CompilerBugError() + + def _translated_string_attrs( + self, translated: T.Optional[T.Union[QuotedLiteral, Translated]] + ) -> T.Dict[str, T.Optional[str]]: + if translated is None: + return {} + elif isinstance(translated, QuotedLiteral): + return {} + else: + return {"translatable": "yes", "context": translated.translate_context} + + def _emit_signal(self, signal: Signal, xml: XmlEmitter): + name = signal.name + if signal.detail_name: + name += "::" + signal.detail_name + xml.put_self_closing( + "signal", + name=name, + handler=signal.handler, + swapped=signal.is_swapped or None, + after=signal.is_after or None, + object=( + self._object_id(signal, signal.object_id) if signal.object_id else None + ), + ) + + def _emit_child(self, child: Child, xml: XmlEmitter): + child_type = internal_child = None + if child.annotation is not None: + annotation = child.annotation.child + if isinstance(annotation, ChildType): + child_type = annotation.child_type + elif isinstance(annotation, ChildInternal): + internal_child = annotation.internal_child + elif isinstance(annotation, ChildExtension): + child_type = "action" + else: + raise CompilerBugError() + + xml.start_tag("child", type=child_type, internal_child=internal_child) + self._emit_object(child.object, xml) + xml.end_tag() + + def _emit_literal(self, literal: Literal, xml: XmlEmitter): + value = literal.value + if isinstance(value, IdentLiteral): + value_type = value.context[ValueTypeCtx].value_type + if isinstance(value_type, gir.BoolType): + xml.put_text(value.ident) + elif isinstance(value_type, gir.Enumeration): + xml.put_text(str(value_type.members[value.ident].value)) + else: + xml.put_text(self._object_id(value, value.ident)) + elif isinstance(value, TypeLiteral): + xml.put_text(value.type_name.glib_type_name) + else: + if isinstance(value.value, float) and value.value == int(value.value): + xml.put_text(int(value.value)) + else: + xml.put_text(value.value) + + def _emit_value(self, value: Value, xml: XmlEmitter): + if isinstance(value.child, Literal): + self._emit_literal(value.child, xml) + elif isinstance(value.child, Flags): + xml.put_text( + "|".join([str(flag.value or flag.name) for flag in value.child.flags]) + ) + elif isinstance(value.child, Translated): + raise CompilerBugError("translated values must be handled in the parent") + elif isinstance(value.child, TypeLiteral): + xml.put_text(value.child.type_name.glib_type_name) + elif isinstance(value.child, ObjectValue): + self._emit_object(value.child.object, xml) + else: + raise CompilerBugError() + + def _emit_expression(self, expression: Expression, xml: XmlEmitter): + self._emit_expression_part(expression.last, xml) + + def _emit_expression_part(self, expression: ExprBase, xml: XmlEmitter): + if isinstance(expression, LiteralExpr): + self._emit_literal_expr(expression, xml) + elif isinstance(expression, LookupOp): + self._emit_lookup_op(expression, xml) + elif isinstance(expression, Expression): + self._emit_expression(expression, xml) + elif isinstance(expression, CastExpr): + self._emit_cast_expr(expression, xml) + elif isinstance(expression, ClosureExpr): + self._emit_closure_expr(expression, xml) + else: + raise CompilerBugError() + + def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter): + if expr.is_object: + xml.start_tag("constant") + else: + xml.start_tag("constant", type=expr.type) + self._emit_literal(expr.literal, xml) + xml.end_tag() + + def _emit_lookup_op(self, expr: LookupOp, xml: XmlEmitter): + xml.start_tag("lookup", name=expr.property_name, type=expr.lhs.type) + self._emit_expression_part(expr.lhs, xml) + xml.end_tag() + + def _emit_cast_expr(self, expr: CastExpr, xml: XmlEmitter): + self._emit_expression_part(expr.lhs, xml) + + def _emit_closure_expr(self, expr: ClosureExpr, xml: XmlEmitter): + xml.start_tag("closure", function=expr.closure_name, type=expr.type) + for arg in expr.args: + self._emit_expression_part(arg.expr, xml) + xml.end_tag() + + def _emit_attribute( + self, + tag: str, + attr: str, + name: str, + value: T.Union[Value, StringValue], + xml: XmlEmitter, + ): + attrs = {attr: name} + + if isinstance(value.child, Translated): + xml.start_tag(tag, **attrs, **self._translated_string_attrs(value.child)) + xml.put_text(value.child.string) + xml.end_tag() + elif isinstance(value.child, QuotedLiteral): + xml.start_tag(tag, **attrs) + xml.put_text(value.child.value) + xml.end_tag() + else: + xml.start_tag(tag, **attrs) + self._emit_value(value, xml) + xml.end_tag() + + def _emit_extensions(self, extension, xml: XmlEmitter): + if isinstance(extension, ExtAccessibility): + xml.start_tag("accessibility") + for property in extension.properties: + for val in property.values: + self._emit_attribute( + property.tag_name, "name", property.name, val, xml + ) + xml.end_tag() + + elif isinstance(extension, AdwBreakpointCondition): + xml.start_tag("condition") + xml.put_text(extension.condition) + xml.end_tag() + + elif isinstance(extension, AdwBreakpointSetters): + for setter in extension.setters: + attrs = {} + + if isinstance(setter.value.child, Translated): + attrs = self._translated_string_attrs(setter.value.child) + + xml.start_tag( + "setter", + object=self._object_id(setter, setter.object_id), + property=setter.property_name, + **attrs, + ) + if isinstance(setter.value.child, Translated): + xml.put_text(setter.value.child.string) + elif ( + isinstance(setter.value.child, Literal) + and isinstance(setter.value.child.value, IdentLiteral) + and setter.value.child.value.ident == "null" + and setter.context[ScopeCtx].objects.get("null") is None + ): + pass + else: + self._emit_value(setter.value, xml) + xml.end_tag() + + elif isinstance(extension, Filters): + xml.start_tag(extension.tokens["tag_name"]) + for prop in extension.children: + xml.start_tag(prop.tokens["tag_name"]) + xml.put_text(prop.tokens["name"]) + xml.end_tag() + xml.end_tag() + + elif isinstance(extension, ExtComboBoxItems): + xml.start_tag("items") + for prop in extension.children: + self._emit_attribute("item", "id", prop.name, prop.value, xml) + xml.end_tag() + + elif isinstance(extension, ExtLayout): + xml.start_tag("layout") + for prop in extension.children: + self._emit_attribute("property", "name", prop.name, prop.value, xml) + xml.end_tag() + + elif isinstance(extension, ExtAdwResponseDialog): + xml.start_tag("responses") + for response in extension.responses: + xml.start_tag( + "response", + id=response.id, + **self._translated_string_attrs(response.value.child), + enabled=None if response.enabled else "false", + appearance=response.appearance, + ) + xml.put_text(response.value.string) + xml.end_tag() + xml.end_tag() + + elif isinstance(extension, ExtScaleMarks): + xml.start_tag("marks") + for mark in extension.children: + xml.start_tag( + "mark", + value=mark.value, + position=mark.position, + **self._translated_string_attrs(mark.label and mark.label.child), + ) + if mark.label is not None: + xml.put_text(mark.label.string) + xml.end_tag() + xml.end_tag() + + elif isinstance(extension, ExtStringListStrings): + xml.start_tag("items") + for string in extension.children: + value = string.child + xml.start_tag("item", **self._translated_string_attrs(value.child)) + xml.put_text(value.string) + xml.end_tag() + xml.end_tag() + + elif isinstance(extension, ExtListItemFactory): + child_xml = XmlEmitter(generated_notice=False) + child_xml.start_tag("interface") + child_xml.start_tag("template", **{"class": extension.gir_class}) + self._emit_object_or_template(extension, child_xml) + child_xml.end_tag() + child_xml.end_tag() + xml.start_tag("property", name="bytes") + xml.put_cdata(child_xml.result) + xml.end_tag() + + elif isinstance(extension, ExtStyles): + xml.start_tag("style") + for style in extension.children: + xml.put_self_closing("class", name=style.name) + xml.end_tag() + + elif isinstance(extension, ExtSizeGroupWidgets): + xml.start_tag("widgets") + for prop in extension.children: + xml.put_self_closing("widget", name=prop.name) + xml.end_tag() + + else: + raise CompilerBugError() + + def _object_id(self, node: AstNode, id: str) -> str: + if id == "template" and node.context[ScopeCtx].template is not None: + return node.context[ScopeCtx].template.gir_class.glib_type_name + else: + return id diff --git a/gtk/blueprint-compiler/blueprintcompiler/outputs/xml/xml_emitter.py b/gtk/blueprint-compiler/blueprintcompiler/outputs/xml/xml_emitter.py new file mode 100644 index 00000000000..ca87a4990c3 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/outputs/xml/xml_emitter.py @@ -0,0 +1,87 @@ +# xml_emitter.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import typing as T +from xml.sax import saxutils + +from blueprintcompiler.gir import GirType +from blueprintcompiler.language.types import ClassName + + +class XmlEmitter: + def __init__(self, indent=2, generated_notice=True): + self.indent = indent + self.result = '' + if generated_notice: + self.result += ( + "\n" + "" + ) + self._tag_stack = [] + self._needs_newline = False + + def start_tag(self, tag, **attrs: T.Union[str, GirType, ClassName, bool, None]): + self._indent() + self.result += f"<{tag}" + for key, val in attrs.items(): + if val is not None: + self.result += f' {key.replace("_", "-")}="{saxutils.escape(self._to_string(val))}"' + self.result += ">" + self._tag_stack.append(tag) + self._needs_newline = False + + def put_self_closing(self, tag, **attrs): + self._indent() + self.result += f"<{tag}" + for key, val in attrs.items(): + if val is not None: + self.result += f' {key.replace("_", "-")}="{saxutils.escape(self._to_string(val))}"' + self.result += "/>" + self._needs_newline = True + + def end_tag(self): + tag = self._tag_stack.pop() + if self._needs_newline: + self._indent() + self.result += f"" + self._needs_newline = True + + def put_text(self, text: T.Union[str, int, float]): + self.result += saxutils.escape(str(text)) + self._needs_newline = False + + def put_cdata(self, text: str): + self.result += f"" + self._needs_newline = False + + def _indent(self): + if self.indent is not None: + self.result += "\n" + " " * (self.indent * len(self._tag_stack)) + + def _to_string(self, val): + if isinstance(val, GirType): + return val.glib_type_name + elif isinstance(val, ClassName): + return val.glib_type_name + else: + return str(val) diff --git a/gtk/blueprint-compiler/blueprintcompiler/parse_tree.py b/gtk/blueprint-compiler/blueprintcompiler/parse_tree.py new file mode 100644 index 00000000000..fff6e4a5f2b --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/parse_tree.py @@ -0,0 +1,653 @@ +# parse_tree.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +""" Utilities for parsing an AST from a token stream. """ + +import typing as T +from enum import Enum + +from . import utils +from .ast_utils import AstNode +from .errors import ( + CompileError, + CompilerBugError, + CompileWarning, + UnexpectedTokenError, + assert_true, +) +from .tokenizer import Range, Token, TokenType + +SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE] + + +class ParseResult(Enum): + """Represents the result of parsing. The extra EMPTY result is necessary + to avoid freezing the parser: imagine a ZeroOrMore node containing a node + that can match empty. It will repeatedly match empty and never advance + the parser. So, ZeroOrMore stops when a failed *or empty* match is + made.""" + + SUCCESS = 0 + FAILURE = 1 + EMPTY = 2 + + def matched(self): + return self == ParseResult.SUCCESS + + def succeeded(self): + return self != ParseResult.FAILURE + + def failed(self): + return self == ParseResult.FAILURE + + +class ParseGroup: + """A matching group. Match groups have an AST type, children grouped by + type, and key=value pairs. At the end of parsing, the match groups will + be converted to AST nodes by passing the children and key=value pairs to + the AST node constructor.""" + + def __init__(self, ast_type: T.Type[AstNode], start: int, text: str): + self.ast_type = ast_type + self.children: T.List[ParseGroup] = [] + self.keys: T.Dict[str, T.Any] = {} + self.tokens: T.Dict[str, T.Optional[Token]] = {} + self.ranges: T.Dict[str, Range] = {} + self.start = start + self.end: T.Optional[int] = None + self.incomplete = False + self.text = text + + def add_child(self, child: "ParseGroup"): + self.children.append(child) + + def set_val(self, key: str, val: T.Any, token: T.Optional[Token]): + assert_true(key not in self.keys) + + self.keys[key] = val + self.tokens[key] = token + if token: + self.set_range(key, token.range) + + def set_range(self, key: str, range: Range): + assert_true(key not in self.ranges) + self.ranges[key] = range + + def to_ast(self): + """Creates an AST node from the match group.""" + children = [child.to_ast() for child in self.children] + + try: + return self.ast_type(self, children, self.keys, incomplete=self.incomplete) + except TypeError as e: + raise CompilerBugError( + f"Failed to construct ast.{self.ast_type.__name__} from ParseGroup. See the previous stacktrace." + ) + + def __str__(self): + result = str(self.ast_type.__name__) + result += "".join([f"\n{key}: {val}" for key, val in self.keys.items()]) + "\n" + result += "\n".join( + [str(child) for children in self.children.values() for child in children] + ) + return result.replace("\n", "\n ") + + +class ParseContext: + """Contains the state of the parser.""" + + def __init__(self, tokens: T.List[Token], text: str, index=0): + self.tokens = tokens + self.text = text + + self.binding_power = 0 + self.index = index + self.start = index + self.group: T.Optional[ParseGroup] = None + self.group_keys: T.Dict[str, T.Tuple[T.Any, T.Optional[Token]]] = {} + self.group_children: T.List[ParseGroup] = [] + self.group_ranges: T.Dict[str, Range] = {} + self.last_group: T.Optional[ParseGroup] = None + self.group_incomplete = False + + self.errors: T.List[CompileError] = [] + self.warnings: T.List[CompileWarning] = [] + + def create_child(self) -> "ParseContext": + """Creates a new ParseContext at this context's position. The new + context will be used to parse one node. If parsing is successful, the + new context will be applied to "self". If parsing fails, the new + context will be discarded.""" + ctx = ParseContext(self.tokens, self.text, self.index) + ctx.errors = self.errors + ctx.warnings = self.warnings + ctx.binding_power = self.binding_power + return ctx + + def apply_child(self, other: "ParseContext"): + """Applies a child context to this context.""" + + if other.group is not None: + # If the other context had a match group, collect all the matched + # values into it and then add it to our own match group. + for key, (val, token) in other.group_keys.items(): + other.group.set_val(key, val, token) + for child in other.group_children: + other.group.add_child(child) + for key, range in other.group_ranges.items(): + other.group.set_range(key, range) + other.group.end = other.tokens[other.index - 1].end + other.group.incomplete = other.group_incomplete + self.group_children.append(other.group) + else: + # If the other context had no match group of its own, collect all + # its matched values + self.group_keys = {**self.group_keys, **other.group_keys} + self.group_children += other.group_children + self.group_ranges = {**self.group_ranges, **other.group_ranges} + self.group_incomplete |= other.group_incomplete + + self.index = other.index + # Propagate the last parsed group down the stack so it can be easily + # retrieved at the end of the process + if other.group: + self.last_group = other.group + elif other.last_group: + self.last_group = other.last_group + + def start_group(self, ast_type: T.Type[AstNode]): + """Sets this context to have its own match group.""" + assert_true(self.group is None) + self.group = ParseGroup(ast_type, self.tokens[self.index].start, self.text) + + def set_group_val(self, key: str, value: T.Any, token: T.Optional[Token]): + """Sets a matched key=value pair on the current match group.""" + assert_true(key not in self.group_keys) + self.group_keys[key] = (value, token) + + def set_mark(self, key: str): + """Sets a zero-length range on the current match group at the current position.""" + self.group_ranges[key] = Range( + self.tokens[self.index].start, self.tokens[self.index].start, self.text + ) + + def set_group_incomplete(self): + """Marks the current match group as incomplete (it could not be fully + parsed, but the parser recovered).""" + self.group_incomplete = True + + def skip(self): + """Skips whitespace and comments.""" + while ( + self.index < len(self.tokens) + and self.tokens[self.index].type in SKIP_TOKENS + ): + self.index += 1 + + def next_token(self) -> Token: + """Advances the token iterator and returns the next token.""" + self.skip() + token = self.tokens[self.index] + self.index += 1 + return token + + def peek_token(self) -> Token: + """Returns the next token without advancing the iterator.""" + self.skip() + token = self.tokens[self.index] + return token + + def skip_unexpected_token(self): + """Skips a token and logs an "unexpected token" error.""" + + self.skip() + start = self.tokens[self.index].start + self.next_token() + self.skip() + end = self.tokens[self.index - 1].end + + if ( + len(self.errors) + and isinstance((err := self.errors[-1]), UnexpectedTokenError) + and err.range.end == start + ): + err.range.end = end + else: + self.errors.append(UnexpectedTokenError(Range(start, end, self.text))) + + def is_eof(self) -> bool: + return self.index >= len(self.tokens) or self.peek_token().type == TokenType.EOF + + +class ParseNode: + """Base class for the nodes in the parser tree.""" + + def parse(self, ctx: ParseContext) -> ParseResult: + """Attempts to match the ParseNode at the context's current location.""" + start_idx = ctx.index + inner_ctx = ctx.create_child() + + if self._parse(inner_ctx): + ctx.apply_child(inner_ctx) + if ctx.index == start_idx: + return ParseResult.EMPTY + else: + return ParseResult.SUCCESS + else: + return ParseResult.FAILURE + + def _parse(self, ctx: ParseContext) -> bool: + raise NotImplementedError() + + def err(self, message: str) -> "Err": + """Causes this ParseNode to raise an exception if it fails to parse. + This prevents the parser from backtracking, so you should understand + what it does and how the parser works before using it.""" + return Err(self, message) + + def expected(self, expect: str) -> "Err": + """Convenience method for err().""" + return self.err("Expected " + expect) + + def warn(self, message) -> "Warning": + """Causes this ParseNode to emit a warning if it parses successfully.""" + return Warning(self, message) + + +class Err(ParseNode): + """ParseNode that emits a compile error if it fails to parse.""" + + def __init__(self, child, message: str): + self.child = to_parse_node(child) + self.message = message + + def _parse(self, ctx: ParseContext): + if self.child.parse(ctx).failed(): + start_idx = ctx.start + while ctx.tokens[start_idx].type in SKIP_TOKENS: + start_idx += 1 + start_token = ctx.tokens[start_idx] + + raise CompileError( + self.message, Range(start_token.start, start_token.start, ctx.text) + ) + return True + + +class Warning(ParseNode): + """ParseNode that emits a compile warning if it parses successfully.""" + + def __init__(self, child, message: str): + self.child = to_parse_node(child) + self.message = message + + def _parse(self, ctx: ParseContext): + ctx.skip() + start_idx = ctx.index + if self.child.parse(ctx).succeeded(): + start_token = ctx.tokens[start_idx] + end_token = ctx.tokens[ctx.index] + ctx.warnings.append( + CompileWarning(self.message, start_token.start, end_token.end) + ) + return True + else: + return False + + +class Fail(ParseNode): + """ParseNode that emits a compile error if it parses successfully.""" + + def __init__(self, child, message: str): + self.child = to_parse_node(child) + self.message = message + + def _parse(self, ctx: ParseContext): + if self.child.parse(ctx).succeeded(): + start_idx = ctx.start + while ctx.tokens[start_idx].type in SKIP_TOKENS: + start_idx += 1 + + start_token = ctx.tokens[start_idx] + end_token = ctx.tokens[ctx.index] + raise CompileError( + self.message, Range.join(start_token.range, end_token.range) + ) + return True + + +class Group(ParseNode): + """ParseNode that creates a match group.""" + + def __init__(self, ast_type: T.Type[AstNode], child): + self.ast_type = ast_type + self.child = to_parse_node(child) + + def _parse(self, ctx: ParseContext) -> bool: + ctx.skip() + ctx.start_group(self.ast_type) + return self.child.parse(ctx).succeeded() + + +class Sequence(ParseNode): + """ParseNode that attempts to match all of its children in sequence.""" + + def __init__(self, *children): + self.children = [to_parse_node(child) for child in children] + + def _parse(self, ctx) -> bool: + for child in self.children: + if child.parse(ctx).failed(): + return False + return True + + +class Statement(ParseNode): + """ParseNode that attempts to match all of its children in sequence. If any + child raises an error, the error will be logged but parsing will continue.""" + + def __init__(self, *children): + self.children = [to_parse_node(child) for child in children] + + def _parse(self, ctx) -> bool: + for child in self.children: + try: + if child.parse(ctx).failed(): + return False + except CompileError as e: + ctx.errors.append(e) + ctx.set_group_incomplete() + return True + + token = ctx.peek_token() + if str(token) != ";": + ctx.errors.append(CompileError("Expected `;`", token.range)) + else: + ctx.next_token() + return True + + +class AnyOf(ParseNode): + """ParseNode that attempts to match exactly one of its children. Child + nodes are attempted in order.""" + + def __init__(self, *children): + self.children = children + + @property + def children(self): + return self._children + + @children.setter + def children(self, children): + self._children = [to_parse_node(child) for child in children] + + def _parse(self, ctx): + for child in self.children: + if child.parse(ctx).succeeded(): + return True + return False + + +class Until(ParseNode): + """ParseNode that repeats its child until a delimiting token is found. If + the child does not match, one token is skipped and the match is attempted + again.""" + + def __init__(self, child, delimiter, between_delimiter=None): + self.child = to_parse_node(child) + self.delimiter = to_parse_node(delimiter) + self.between_delimiter = ( + to_parse_node(between_delimiter) if between_delimiter is not None else None + ) + + def _parse(self, ctx: ParseContext): + while not self.delimiter.parse(ctx).succeeded(): + if ctx.is_eof(): + return False + + try: + if not self.child.parse(ctx).matched(): + ctx.skip_unexpected_token() + + if ( + self.between_delimiter is not None + and not self.between_delimiter.parse(ctx).succeeded() + ): + if self.delimiter.parse(ctx).succeeded(): + return True + else: + if ctx.is_eof(): + return False + ctx.skip_unexpected_token() + except CompileError as e: + ctx.errors.append(e) + ctx.next_token() + + return True + + +class ZeroOrMore(ParseNode): + """ParseNode that matches its child any number of times (including zero + times). It cannot fail to parse. If its child raises an exception, one token + will be skipped and parsing will continue.""" + + def __init__(self, child): + self.child = to_parse_node(child) + + def _parse(self, ctx): + while True: + try: + if not self.child.parse(ctx).matched(): + return True + except CompileError as e: + ctx.errors.append(e) + ctx.next_token() + + +class Delimited(ParseNode): + """ParseNode that matches its first child any number of times (including zero + times) with its second child in between and optionally at the end.""" + + def __init__(self, child, delimiter): + self.child = to_parse_node(child) + self.delimiter = to_parse_node(delimiter) + + def _parse(self, ctx): + while self.child.parse(ctx).matched() and self.delimiter.parse(ctx).matched(): + pass + return True + + +class Optional(ParseNode): + """ParseNode that matches its child zero or one times. It cannot fail to + parse.""" + + def __init__(self, child): + self.child = to_parse_node(child) + + def _parse(self, ctx): + self.child.parse(ctx) + return True + + +class Eof(ParseNode): + """ParseNode that matches an EOF token.""" + + def _parse(self, ctx: ParseContext) -> bool: + token = ctx.next_token() + return token.type == TokenType.EOF + + +class Match(ParseNode): + """ParseNode that matches the given literal token.""" + + def __init__(self, op: str): + self.op = op + + def _parse(self, ctx: ParseContext) -> bool: + token = ctx.next_token() + return str(token) == self.op + + def expected(self, expect: T.Optional[str] = None): + """Convenience method for err().""" + if expect is None: + return self.err(f"Expected '{self.op}'") + else: + return self.err("Expected " + expect) + + +class UseIdent(ParseNode): + """ParseNode that matches any identifier and sets it in a key=value pair on + the containing match group.""" + + def __init__(self, key: str): + self.key = key + + def _parse(self, ctx: ParseContext): + token = ctx.next_token() + if token.type != TokenType.IDENT: + return False + + ctx.set_group_val(self.key, str(token), token) + return True + + +class UseNumber(ParseNode): + """ParseNode that matches a number and sets it in a key=value pair on + the containing match group.""" + + def __init__(self, key: str): + self.key = key + + def _parse(self, ctx: ParseContext): + token = ctx.next_token() + if token.type != TokenType.NUMBER: + return False + + number = token.get_number() + ctx.set_group_val(self.key, number, token) + return True + + +class UseNumberText(ParseNode): + """ParseNode that matches a number, but sets its *original text* it in a + key=value pair on the containing match group.""" + + def __init__(self, key: str): + self.key = key + + def _parse(self, ctx: ParseContext): + token = ctx.next_token() + if token.type != TokenType.NUMBER: + return False + + ctx.set_group_val(self.key, str(token), token) + return True + + +class UseQuoted(ParseNode): + """ParseNode that matches a quoted string and sets it in a key=value pair + on the containing match group.""" + + def __init__(self, key: str): + self.key = key + + def _parse(self, ctx: ParseContext): + token = ctx.next_token() + if token.type != TokenType.QUOTED: + return False + + unescaped = None + + try: + unescaped = utils.unescape_quote(str(token)) + except utils.UnescapeError as e: + start = ctx.tokens[ctx.index - 1].start + range = Range(start + e.start, start + e.end, ctx.text) + ctx.errors.append( + CompileError(f"Invalid escape sequence '{range.text}'", range) + ) + + ctx.set_group_val(self.key, unescaped, token) + + return True + + +class UseLiteral(ParseNode): + """ParseNode that doesn't match anything, but rather sets a static key=value + pair on the containing group. Useful for, e.g., property and signal flags: + `Sequence(Keyword("swapped"), UseLiteral("swapped", True))`""" + + def __init__(self, key: str, literal: T.Any): + self.key = key + self.literal = literal + + def _parse(self, ctx: ParseContext): + ctx.set_group_val(self.key, self.literal, None) + return True + + +class UseExact(ParseNode): + """Matches the given identifier and sets it as a named token.""" + + def __init__(self, key: str, string: str): + self.key = key + self.string = string + + def _parse(self, ctx: ParseContext): + token = ctx.next_token() + ctx.set_group_val(self.key, self.string, token) + return str(token) == self.string + + +class Keyword(ParseNode): + """Matches the given identifier and sets it as a named token, with the name + being the identifier itself.""" + + def __init__(self, kw: str): + self.kw = kw + self.set_token = True + + def _parse(self, ctx: ParseContext): + token = ctx.next_token() + ctx.set_group_val(self.kw, True, token) + return str(token) == self.kw + + +class Mark(ParseNode): + def __init__(self, key: str): + self.key = key + + def _parse(self, ctx: ParseContext): + ctx.set_mark(self.key) + return True + + +def to_parse_node(value) -> ParseNode: + if isinstance(value, str): + return Match(value) + elif isinstance(value, list): + return Sequence(*value) + elif isinstance(value, type) and hasattr(value, "grammar"): + return Group(value, getattr(value, "grammar")) + elif isinstance(value, ParseNode): + return value + else: + raise CompilerBugError() diff --git a/gtk/blueprint-compiler/blueprintcompiler/parser.py b/gtk/blueprint-compiler/blueprintcompiler/parser.py new file mode 100644 index 00000000000..1f87647e206 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/parser.py @@ -0,0 +1,47 @@ +# parser.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .errors import MultipleErrors, PrintableError +from .language import OBJECT_CONTENT_HOOKS, UI, Template +from .parse_tree import * +from .tokenizer import TokenType + + +def parse( + tokens: T.List[Token], +) -> T.Tuple[T.Optional[UI], T.Optional[MultipleErrors], T.List[CompileError]]: + """Parses a list of tokens into an abstract syntax tree.""" + + try: + original_text = tokens[0].string if len(tokens) else "" + ctx = ParseContext(tokens, original_text) + AnyOf(UI).parse(ctx) + + assert ctx.last_group is not None + ast_node = ctx.last_group.to_ast() + + errors = [*ctx.errors, *ast_node.errors] + warnings = [*ctx.warnings, *ast_node.warnings] + + return (ast_node, MultipleErrors(errors) if len(errors) else None, warnings) + except MultipleErrors as e: + return (None, e, []) + except CompileError as e: + return (None, MultipleErrors([e]), []) diff --git a/gtk/blueprint-compiler/blueprintcompiler/tokenizer.py b/gtk/blueprint-compiler/blueprintcompiler/tokenizer.py new file mode 100644 index 00000000000..85bce954a82 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/tokenizer.py @@ -0,0 +1,155 @@ +# tokenizer.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +import re +import typing as T +from dataclasses import dataclass +from enum import Enum + +from . import utils + + +class TokenType(Enum): + EOF = 0 + IDENT = 1 + QUOTED = 2 + NUMBER = 3 + OP = 4 + WHITESPACE = 5 + COMMENT = 6 + PUNCTUATION = 7 + + +_tokens = [ + (TokenType.IDENT, r"[A-Za-z_][\d\w\-_]*"), + (TokenType.QUOTED, r'"(\\(.|\n)|[^\\"\n])*"'), + (TokenType.QUOTED, r"'(\\(.|\n)|[^\\'\n])*'"), + (TokenType.NUMBER, r"0x[A-Za-z0-9_]+"), + (TokenType.NUMBER, r"[\d_]+(\.[\d_]+)?"), + (TokenType.NUMBER, r"\.[\d_]+"), + (TokenType.WHITESPACE, r"\s+"), + (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), + (TokenType.COMMENT, r"\/\/[^\n]*"), + (TokenType.OP, r"\$|<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"), + (TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"), +] +_TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens] + + +class Token: + def __init__(self, type: TokenType, start: int, end: int, string: str): + self.type = type + self.start = start + self.end = end + self.string = string + + def __str__(self) -> str: + return self.string[self.start : self.end] + + @property + def range(self) -> "Range": + return Range(self.start, self.end, self.string) + + def get_number(self) -> T.Union[int, float]: + from .errors import CompileError, CompilerBugError + + if self.type != TokenType.NUMBER: + raise CompilerBugError() + + string = str(self).replace("_", "") + try: + if string.startswith("0x"): + return int(string, 16) + elif "." in string: + return float(string) + else: + return int(string) + except: + raise CompileError(f"{str(self)} is not a valid number literal", self.range) + + +def _tokenize(ui_ml: str): + from .errors import CompileError + + i = 0 + while i < len(ui_ml): + matched = False + for type, regex in _TOKENS: + match = regex.match(ui_ml, i) + + if match is not None: + yield Token(type, match.start(), match.end(), ui_ml) + i = match.end() + matched = True + break + + if not matched: + raise CompileError( + "Could not determine what kind of syntax is meant here", + Range(i, i, ui_ml), + ) + + yield Token(TokenType.EOF, i, i, ui_ml) + + +def tokenize(data: str) -> T.List[Token]: + return list(_tokenize(data)) + + +@dataclass +class Range: + start: int + end: int + original_text: str + + @property + def length(self) -> int: + return self.end - self.start + + @property + def text(self) -> str: + return self.original_text[self.start : self.end] + + @property + def with_trailing_newline(self) -> "Range": + if len(self.original_text) > self.end and self.original_text[self.end] == "\n": + return Range(self.start, self.end + 1, self.original_text) + else: + return self + + @staticmethod + def join(a: T.Optional["Range"], b: T.Optional["Range"]) -> T.Optional["Range"]: + if a is None: + return b + if b is None: + return a + return Range(min(a.start, b.start), max(a.end, b.end), a.original_text) + + def __contains__(self, other: T.Union[int, "Range"]) -> bool: + if isinstance(other, int): + return self.start <= other <= self.end + else: + return self.start <= other.start and self.end >= other.end + + def to_json(self): + return utils.idxs_to_range(self.start, self.end, self.original_text) + + def overlaps(self, other: "Range") -> bool: + return not (self.end < other.start or self.start > other.end) diff --git a/gtk/blueprint-compiler/blueprintcompiler/typelib.py b/gtk/blueprint-compiler/blueprintcompiler/typelib.py new file mode 100644 index 00000000000..be22eb1d04d --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/typelib.py @@ -0,0 +1,322 @@ +# typelib.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import math +import mmap +import os +import sys +import typing as T +from ctypes import * + +from .errors import CompilerBugError + +BLOB_TYPE_STRUCT = 3 +BLOB_TYPE_BOXED = 4 +BLOB_TYPE_ENUM = 5 +BLOB_TYPE_FLAGS = 6 +BLOB_TYPE_OBJECT = 7 +BLOB_TYPE_INTERFACE = 8 + +TYPE_VOID = 0 +TYPE_BOOLEAN = 1 +TYPE_INT8 = 2 +TYPE_UINT8 = 3 +TYPE_INT16 = 4 +TYPE_UINT16 = 5 +TYPE_INT32 = 6 +TYPE_UINT32 = 7 +TYPE_INT64 = 8 +TYPE_UINT64 = 9 +TYPE_FLOAT = 10 +TYPE_DOUBLE = 11 +TYPE_GTYPE = 12 +TYPE_UTF8 = 13 +TYPE_FILENAME = 14 +TYPE_ARRAY = 15 +TYPE_INTERFACE = 16 +TYPE_GLIST = 17 +TYPE_GSLIST = 18 +TYPE_GHASH = 19 +TYPE_ERROR = 20 +TYPE_UNICHAR = 21 + + +class Field: + def __init__(self, offset: int, type: str, shift=0, mask=None): + self._offset = offset + self._type = type + if not mask or sys.byteorder == "little": + self._shift = shift + elif self._type == "u8" or self._type == "i8": + self._shift = 8 - (shift + mask) + elif self._type == "u16" or self._type == "i16": + self._shift = 16 - (shift + mask) + else: + self._shift = 32 - (shift + mask) + self._mask = (1 << mask) - 1 if mask else None + self._name = f"{offset}__{type}__{shift}__{mask}" + + def __get__(self, typelib: "Typelib", _objtype=None): + if typelib is None: + return self + + def shift_mask(n): + n = n >> self._shift + if self._mask: + n = n & self._mask + return n + + tl = typelib[self._offset] + if self._type == "u8": + return shift_mask(tl.u8) + elif self._type == "u16": + return shift_mask(tl.u16) + elif self._type == "u32": + return shift_mask(tl.u32) + elif self._type == "i8": + return shift_mask(tl.i8) + elif self._type == "i16": + return shift_mask(tl.i16) + elif self._type == "i32": + return shift_mask(tl.i32) + elif self._type == "pointer": + return tl.header[tl.u32] + elif self._type == "offset": + return tl + elif self._type == "string": + return tl.string + elif self._type == "dir_entry": + return tl.header.dir_entry(tl.u16) + else: + raise CompilerBugError(self._type) + + +class Typelib: + AS_DIR_ENTRY = Field(0, "dir_entry") + + HEADER_N_ENTRIES = Field(0x14, "u16") + HEADER_N_LOCAL_ENTRIES = Field(0x16, "u16") + HEADER_DIRECTORY = Field(0x18, "pointer") + HEADER_N_ATTRIBUTES = Field(0x1C, "u32") + HEADER_ATTRIBUTES = Field(0x20, "pointer") + + HEADER_DEPENDENCIES = Field(0x24, "pointer") + + HEADER_NAMESPACE = Field(0x2C, "string") + HEADER_NSVERSION = Field(0x30, "string") + + HEADER_ENTRY_BLOB_SIZE = Field(0x3C, "u16") + HEADER_FUNCTION_BLOB_SIZE = Field(0x3E, "u16") + HEADER_CALLBACK_BLOB_SIZE = Field(0x40, "u16") + HEADER_SIGNAL_BLOB_SIZE = Field(0x42, "u16") + HEADER_ARG_BLOB_SIZE = Field(0x46, "u16") + HEADER_PROPERTY_BLOB_SIZE = Field(0x48, "u16") + HEADER_FIELD_BLOB_SIZE = Field(0x4A, "u16") + HEADER_VALUE_BLOB_SIZE = Field(0x4C, "u16") + HEADER_ATTRIBUTE_BLOB_SIZE = Field(0x4E, "u16") + HEADER_ENUM_BLOB_SIZE = Field(0x56, "u16") + HEADER_OBJECT_BLOB_SIZE = Field(0x5A, "u16") + HEADER_INTERFACE_BLOB_SIZE = Field(0x5C, "u16") + + DIR_ENTRY_BLOB_TYPE = Field(0x0, "u16") + DIR_ENTRY_LOCAL = Field(0x2, "u16", 0, 1) + DIR_ENTRY_NAME = Field(0x4, "string") + DIR_ENTRY_OFFSET = Field(0x8, "pointer") + DIR_ENTRY_NAMESPACE = Field(0x8, "string") + + ARG_NAME = Field(0x0, "string") + ARG_TYPE = Field(0xC, "u32") + + SIGNATURE_RETURN_TYPE = Field(0x0, "u32") + SIGNATURE_N_ARGUMENTS = Field(0x6, "u16") + SIGNATURE_ARGUMENTS = Field(0x8, "offset") + + ATTR_OFFSET = Field(0x0, "u32") + ATTR_NAME = Field(0x4, "string") + ATTR_VALUE = Field(0x8, "string") + + TYPE_BLOB_TAG = Field(0x0, "u8", 3, 5) + TYPE_BLOB_INTERFACE = Field(0x2, "dir_entry") + TYPE_BLOB_ARRAY_INNER = Field(0x4, "u32") + + BLOB_NAME = Field(0x4, "string") + + STRUCT_DEPRECATED = Field(0x2, "u16", 0, 1) + + ENUM_DEPRECATED = Field(0x2, "u16", 0, 1) + ENUM_GTYPE_NAME = Field(0x8, "string") + ENUM_N_VALUES = Field(0x10, "u16") + ENUM_N_METHODS = Field(0x12, "u16") + ENUM_VALUES = Field(0x18, "offset") + + INTERFACE_DEPRECATED = Field(0x2, "u16", 0, 1) + INTERFACE_GTYPE_NAME = Field(0x8, "string") + INTERFACE_N_PREREQUISITES = Field(0x12, "u16") + INTERFACE_N_PROPERTIES = Field(0x14, "u16") + INTERFACE_N_METHODS = Field(0x16, "u16") + INTERFACE_N_SIGNALS = Field(0x18, "u16") + INTERFACE_N_VFUNCS = Field(0x1A, "u16") + INTERFACE_N_CONSTANTS = Field(0x1C, "u16") + INTERFACE_PREREQUISITES = Field(0x28, "offset") + + OBJ_DEPRECATED = Field(0x02, "u16", 0, 1) + OBJ_ABSTRACT = Field(0x02, "u16", 1, 1) + OBJ_FUNDAMENTAL = Field(0x02, "u16", 2, 1) + OBJ_FINAL = Field(0x02, "u16", 3, 1) + OBJ_GTYPE_NAME = Field(0x08, "string") + OBJ_PARENT = Field(0x10, "dir_entry") + OBJ_GTYPE_STRUCT = Field(0x12, "string") + OBJ_N_INTERFACES = Field(0x14, "u16") + OBJ_N_FIELDS = Field(0x16, "u16") + OBJ_N_PROPERTIES = Field(0x18, "u16") + OBJ_N_METHODS = Field(0x1A, "u16") + OBJ_N_SIGNALS = Field(0x1C, "u16") + OBJ_N_VFUNCS = Field(0x1E, "u16") + OBJ_N_CONSTANTS = Field(0x20, "u16") + OBJ_N_FIELD_CALLBACKS = Field(0x22, "u16") + + PROP_NAME = Field(0x0, "string") + PROP_DEPRECATED = Field(0x4, "u32", 0, 1) + PROP_READABLE = Field(0x4, "u32", 1, 1) + PROP_WRITABLE = Field(0x4, "u32", 2, 1) + PROP_CONSTRUCT = Field(0x4, "u32", 3, 1) + PROP_CONSTRUCT_ONLY = Field(0x4, "u32", 4, 1) + PROP_TYPE = Field(0xC, "u32") + + SIGNAL_DEPRECATED = Field(0x0, "u16", 0, 1) + SIGNAL_DETAILED = Field(0x0, "u16", 5, 1) + SIGNAL_NAME = Field(0x4, "string") + SIGNAL_SIGNATURE = Field(0xC, "pointer") + + VALUE_NAME = Field(0x4, "string") + VALUE_VALUE = Field(0x8, "i32") + + def __init__(self, typelib_file, offset: int): + self._typelib_file = typelib_file + self._offset = offset + + def __getitem__(self, index: int): + return Typelib(self._typelib_file, self._offset + index) + + def attr(self, name): + return self.header.attr(self._offset, name) + + @property + def header(self) -> "TypelibHeader": + return TypelibHeader(self._typelib_file) + + @property + def u8(self) -> int: + """Gets the 8-bit unsigned int at this location.""" + return self._int(1, False) + + @property + def u16(self) -> int: + """Gets the 16-bit unsigned int at this location.""" + return self._int(2, False) + + @property + def u32(self) -> int: + """Gets the 32-bit unsigned int at this location.""" + return self._int(4, False) + + @property + def i8(self) -> int: + """Gets the 8-bit unsigned int at this location.""" + return self._int(1, True) + + @property + def i16(self) -> int: + """Gets the 16-bit unsigned int at this location.""" + return self._int(2, True) + + @property + def i32(self) -> int: + """Gets the 32-bit unsigned int at this location.""" + return self._int(4, True) + + @property + def string(self) -> T.Optional[str]: + """Interprets the 32-bit unsigned int at this location as a pointer + within the typelib file, and returns the null-terminated string at that + pointer.""" + + loc = self.u32 + if loc == 0: + return None + + end = self._typelib_file.find(b"\0", loc) + return self._typelib_file[loc:end].decode("utf-8") + + def _int(self, size, signed) -> int: + return int.from_bytes( + self._typelib_file[self._offset : self._offset + size], + sys.byteorder, + signed=signed, + ) + + +class TypelibHeader(Typelib): + def __init__(self, typelib_file): + super().__init__(typelib_file, 0) + + def dir_entry(self, index) -> T.Optional[Typelib]: + if index == 0: + return None + else: + return self.HEADER_DIRECTORY[(index - 1) * self.HEADER_ENTRY_BLOB_SIZE] + + def attr(self, offset, name): + lower = 0 + upper = self.HEADER_N_ATTRIBUTES + attr_size = self.HEADER_ATTRIBUTE_BLOB_SIZE + attrs = self.HEADER_ATTRIBUTES + mid = 0 + + while lower <= upper: + mid = math.floor((upper + lower) / 2) + attr = attrs[mid * attr_size] + if attr.ATTR_OFFSET < offset: + lower = mid + 1 + elif attr.ATTR_OFFSET > offset: + upper = mid - 1 + else: + while mid >= 0 and attrs[(mid - 1) * attr_size].ATTR_OFFSET == offset: + mid -= 1 + break + if attrs[mid * attr_size].ATTR_OFFSET != offset: + # no match found + return None + while attrs[mid * attr_size].ATTR_OFFSET == offset: + if attrs[mid * attr_size].ATTR_NAME == name: + return attrs[mid * attr_size].ATTR_VALUE + mid += 1 + return None + + def attr_by_index(self, index): + pass + + @property + def dir_entries(self): + return [self.dir_entry(i) for i in range(self[0x16].u16)] + + +def load_typelib(path: str) -> Typelib: + with open(path, "rb") as f: + return Typelib(f.read(), 0) diff --git a/gtk/blueprint-compiler/blueprintcompiler/utils.py b/gtk/blueprint-compiler/blueprintcompiler/utils.py new file mode 100644 index 00000000000..ea8102eebce --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/utils.py @@ -0,0 +1,156 @@ +# utils.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +import typing as T +from dataclasses import dataclass + + +class Colors: + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[33m" + PURPLE = "\033[35m" + FAINT = "\033[2m" + BOLD = "\033[1m" + BLUE = "\033[34m" + UNDERLINE = "\033[4m" + NO_UNDERLINE = "\033[24m" + CLEAR = "\033[0m" + + +def did_you_mean(word: str, options: T.List[str]) -> T.Optional[str]: + if len(options) == 0: + return None + + def levenshtein(a, b): + # see https://en.wikipedia.org/wiki/Levenshtein_distance + m = len(a) + n = len(b) + + distances = [[0 for j in range(n)] for i in range(m)] + + for i in range(m): + distances[i][0] = i + for j in range(n): + distances[0][j] = j + + for j in range(1, n): + for i in range(1, m): + cost = 0 + if a[i] != b[j]: + if a[i].casefold() == b[j].casefold(): + cost = 1 + else: + cost = 2 + distances[i][j] = min( + distances[i - 1][j] + 2, + distances[i][j - 1] + 2, + distances[i - 1][j - 1] + cost, + ) + + return distances[m - 1][n - 1] + + distances = [(option, levenshtein(word, option)) for option in options] + closest = min(distances, key=lambda item: item[1]) + if closest[1] <= 5: + return closest[0] + return None + + +def idx_to_pos(idx: int, text: str) -> T.Tuple[int, int]: + if idx == 0 or len(text) == 0: + return (0, 0) + line_num = text.count("\n", 0, idx) + 1 + col_num = idx - text.rfind("\n", 0, idx) - 1 + return (line_num - 1, col_num) + + +def pos_to_idx(line: int, col: int, text: str) -> int: + lines = text.splitlines(keepends=True) + return sum([len(line) for line in lines[:line]]) + col + + +def idxs_to_range(start: int, end: int, text: str): + start_l, start_c = idx_to_pos(start, text) + end_l, end_c = idx_to_pos(end, text) + return { + "start": { + "line": start_l, + "character": start_c, + }, + "end": { + "line": end_l, + "character": end_c, + }, + } + + +@dataclass +class UnescapeError(Exception): + start: int + end: int + + +def escape_quote(string: str) -> str: + return ( + '"' + + ( + string.replace("\\", "\\\\") + .replace('"', '\\"') + .replace("\n", "\\n") + .replace("\t", "\\t") + ) + + '"' + ) + + +def unescape_quote(string: str) -> str: + string = string[1:-1] + + REPLACEMENTS = { + "\n": "\n", + "\\": "\\", + "n": "\n", + "t": "\t", + '"': '"', + "'": "'", + } + + result = "" + i = 0 + while i < len(string): + c = string[i] + if c == "\\": + i += 1 + + if i >= len(string): + from .errors import CompilerBugError + + raise CompilerBugError() + + if r := REPLACEMENTS.get(string[i]): + result += r + else: + raise UnescapeError(i, i + 2) + else: + result += c + + i += 1 + + return result diff --git a/gtk/blueprint-compiler/blueprintcompiler/xml_reader.py b/gtk/blueprint-compiler/blueprintcompiler/xml_reader.py new file mode 100644 index 00000000000..a3c0e3e0ac0 --- /dev/null +++ b/gtk/blueprint-compiler/blueprintcompiler/xml_reader.py @@ -0,0 +1,99 @@ +# xml_reader.py +# +# Copyright 2021 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +import typing as T +from collections import defaultdict +from functools import cached_property +from xml import sax + +# To speed up parsing, we ignore all tags except these +PARSE_GIR = set( + [ + "repository", + "namespace", + "class", + "interface", + "property", + "glib:signal", + "include", + "implements", + "type", + "parameter", + "parameters", + "enumeration", + "member", + "bitfield", + ] +) + + +class Element: + def __init__(self, tag: str, attrs: T.Dict[str, str]): + self.tag = tag + self.attrs = attrs + self.children: T.List["Element"] = [] + self.cdata_chunks: T.List[str] = [] + + @cached_property + def cdata(self): + return "".join(self.cdata_chunks) + + def get_elements(self, name: str) -> T.List["Element"]: + return [child for child in self.children if child.tag == name] + + def __getitem__(self, key: str): + return self.attrs.get(key) + + +class Handler(sax.handler.ContentHandler): + def __init__(self): + self.root = None + self.stack = [] + + def startElement(self, name, attrs): + element = Element(name, attrs.copy()) + + if len(self.stack): + last = self.stack[-1] + last.children.append(element) + else: + self.root = element + + self.stack.append(element) + + def endElement(self, name): + self.stack.pop() + + def characters(self, content): + self.stack[-1].cdata_chunks.append(content) + + +def parse(filename): + parser = sax.make_parser() + handler = Handler() + parser.setContentHandler(handler) + parser.parse(filename) + return handler.root + + +def parse_string(xml): + handler = Handler() + parser = sax.parseString(xml, handler) + return handler.root diff --git a/gtk/blueprint-compiler/build-aux/Dockerfile b/gtk/blueprint-compiler/build-aux/Dockerfile new file mode 100644 index 00000000000..617015f22f6 --- /dev/null +++ b/gtk/blueprint-compiler/build-aux/Dockerfile @@ -0,0 +1,13 @@ +FROM fedora:latest + +RUN dnf install -y meson gcc g++ python3-pip gobject-introspection-devel \ + python3-devel python3-gobject git diffutils xorg-x11-server-Xvfb \ + appstream-devel dbus-x11 "dnf-command(builddep)" glslc +RUN dnf build-dep -y gtk4 libadwaita +RUN pip3 install furo mypy sphinx coverage black isort + +COPY install_deps.sh . +RUN ./install_deps.sh + +# The version on PyPI is very old and doesn't install. Use the upstream package registry instead. +RUN pip install pythonfuzz --extra-index-url https://gitlab.com/api/v4/projects/19904939/packages/pypi/simple diff --git a/gtk/blueprint-compiler/build-aux/install_deps.sh b/gtk/blueprint-compiler/build-aux/install_deps.sh new file mode 100755 index 00000000000..342778d7408 --- /dev/null +++ b/gtk/blueprint-compiler/build-aux/install_deps.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -e + +echo "===== Install GTK =====" +git clone --depth=1 https://gitlab.gnome.org/GNOME/gtk.git +cd gtk +meson setup builddir \ + --prefix=/usr \ + -Ddocumentation=true \ + -Dbuild-demos=false \ + -Dbuild-examples=false \ + -Dbuild-tests=false \ + -Dbuild-testsuite=false +ninja -C builddir install +cd - +rm -rf gtk + +echo "===== Install libadwaita =====" +git clone --depth=1 https://gitlab.gnome.org/GNOME/libadwaita.git +cd libadwaita +meson builddir \ + --prefix=/usr +ninja -C builddir install +cd - +rm -rf libadwaita diff --git a/gtk/blueprint-compiler/docs/_static/styles.css b/gtk/blueprint-compiler/docs/_static/styles.css new file mode 100644 index 00000000000..cbe003cae5c --- /dev/null +++ b/gtk/blueprint-compiler/docs/_static/styles.css @@ -0,0 +1,24 @@ +.experimental-admonition { + display: flex; + align-items: center; +} + +.experimental-admonition img { + width: 64px; +} + +p.grammar-block { + font-family: var(--font-stack--monospace); + white-space: pre; + overflow: auto; + font-size: var(--code-font-size); + padding: .625rem .875rem; + line-height: 1.5; + background: #f8f8f8; + border-radius: .2rem; +} + +body:not([data-theme="light"]) .grammar-block { + background: #202020; + color: #d0d0d0; +} diff --git a/gtk/blueprint-compiler/docs/collect-sections.py b/gtk/blueprint-compiler/docs/collect-sections.py new file mode 100755 index 00000000000..e6227e728a2 --- /dev/null +++ b/gtk/blueprint-compiler/docs/collect-sections.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 + +import json +import os +import re +import sys +from dataclasses import dataclass +from pathlib import Path + +__all__ = ["get_docs_section"] + +DOCS_ROOT = "https://jwestman.pages.gitlab.gnome.org/blueprint-compiler" + + +sections: dict[str, "Section"] = {} + + +@dataclass +class Section: + link: str + lines: str + + def to_json(self): + return { + "content": rst_to_md(self.lines), + "link": self.link, + } + + +def load_reference_docs(): + for filename in Path(os.path.dirname(__file__), "reference").glob("*.rst"): + with open(filename) as f: + section_name = None + lines = [] + + def close_section(): + if section_name: + html_file = re.sub(r"\.rst$", ".html", filename.name) + anchor = re.sub(r"[^a-z0-9]+", "-", section_name.lower()) + link = f"{DOCS_ROOT}/reference/{html_file}#{anchor}" + sections[section_name] = Section(link, lines) + + for line in f: + if m := re.match(r"\.\.\s+_(.*):", line): + close_section() + section_name = m.group(1) + lines = [] + else: + lines.append(line) + + close_section() + + +# This isn't a comprehensive rST to markdown converter, it just needs to handle the +# small subset of rST used in the reference docs. +def rst_to_md(lines: list[str]) -> str: + result = "" + + def rst_to_md_inline(line): + line = re.sub(r"``(.*?)``", r"`\1`", line) + line = re.sub( + r":ref:`(.*?)<(.*?)>`", + lambda m: f"[{m.group(1)}]({sections[m.group(2)].link})", + line, + ) + line = re.sub(r"`([^`]*?) <([^`>]*?)>`_", r"[\1](\2)", line) + return line + + i = 0 + n = len(lines) + heading_levels = {} + + def print_block(lang: str = "", code: bool = True, strip_links: bool = False): + nonlocal result, i + block = "" + while i < n: + line = lines[i].rstrip() + if line.startswith(" "): + line = line[3:] + elif line != "": + break + + if strip_links: + line = re.sub(r":ref:`(.*?)<(.*?)>`", r"\1", line) + + if not code: + line = rst_to_md_inline(line) + + block += line + "\n" + i += 1 + + if code: + result += f"```{lang}\n{block.strip()}\n```\n\n" + else: + result += block + + while i < n: + line = lines[i].rstrip() + i += 1 + if line == ".. rst-class:: grammar-block": + print_block("text", strip_links=True) + elif line == ".. code-block:: blueprint": + print_block("blueprint") + elif line == ".. note::": + result += "#### Note\n" + print_block(code=False) + elif m := re.match(r"\.\. image:: (.*)", line): + result += f"![{m.group(1)}]({DOCS_ROOT}/_images/{m.group(1)})\n" + elif i < n and re.match(r"^((-+)|(~+)|(\++))$", lines[i]): + level_char = lines[i][0] + if level_char not in heading_levels: + heading_levels[level_char] = max(heading_levels.values(), default=1) + 1 + result += ( + "#" * heading_levels[level_char] + " " + rst_to_md_inline(line) + "\n" + ) + i += 1 + else: + result += rst_to_md_inline(line) + "\n" + + return result + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: collect_sections.py ") + sys.exit(1) + + outfile = sys.argv[1] + + load_reference_docs() + + # print the sections to a json file + with open(outfile, "w") as f: + json.dump( + {name: section.to_json() for name, section in sections.items()}, f, indent=2 + ) diff --git a/gtk/blueprint-compiler/docs/conf.py b/gtk/blueprint-compiler/docs/conf.py new file mode 100644 index 00000000000..a397da76ab0 --- /dev/null +++ b/gtk/blueprint-compiler/docs/conf.py @@ -0,0 +1,53 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = "Blueprint" +copyright = "2021-2023, James Westman" +author = "James Westman" + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "furo" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +html_css_files = ["styles.css"] diff --git a/gtk/blueprint-compiler/docs/experimental.svg b/gtk/blueprint-compiler/docs/experimental.svg new file mode 100644 index 00000000000..4ebac4550b5 --- /dev/null +++ b/gtk/blueprint-compiler/docs/experimental.svg @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gtk/blueprint-compiler/docs/flatpak.rst b/gtk/blueprint-compiler/docs/flatpak.rst new file mode 100644 index 00000000000..0071d2f5462 --- /dev/null +++ b/gtk/blueprint-compiler/docs/flatpak.rst @@ -0,0 +1,26 @@ +======= +Flatpak +======= + +Flathub's builders don't allow internet access during the build; everything that +goes into the build must be specified in the manifest. This means meson +submodules won't work. Instead, you need to install blueprint-compiler as +a module in your flatpak manifest: + +.. code-block:: json + + { + "name": "blueprint-compiler", + "buildsystem": "meson", + "cleanup": ["*"], + "sources": [ + { + "type": "git", + "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", + "tag": "v0.14.0" + } + ] + } + +You can keep the submodule configuration--Meson will ignore it if +blueprint-compiler is already installed. \ No newline at end of file diff --git a/gtk/blueprint-compiler/docs/index.rst b/gtk/blueprint-compiler/docs/index.rst new file mode 100644 index 00000000000..34b942cd8ff --- /dev/null +++ b/gtk/blueprint-compiler/docs/index.rst @@ -0,0 +1,148 @@ +Overview +======== + +.. warning:: + .. container:: experimental-admonition + + .. image:: experimental.svg + + **Blueprint is still experimental.** Future versions may have breaking changes, and most GTK tutorials use XML syntax. + + +Blueprint is a markup language and compiler for GTK 4 user interfaces. + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + setup + translations + flatpak + reference/index + packaging + + +.. code-block:: blueprint + + using Gtk 4.0; + + template MyAppWindow : ApplicationWindow { + default-width: 600; + default-height: 300; + title: _("Hello, Blueprint!"); + + [titlebar] + HeaderBar {} + + Label { + label: bind MyAppWindow.main_text; + } + } + +Blueprint helps you build user interfaces in GTK quickly and declaratively. +It has modern IDE features like code completion and hover documentation, and +the compiler points out mistakes early on so you can focus on making your app +look amazing. + +Features +-------- + +- **Easy setup.** A porting tool is available to help port your projects from + XML. The compiler's only dependency is Python, and it can be included as + a meson subproject. :doc:`See the Setup page for more information. ` +- **Concise syntax.** No more clumsy XML! Blueprint is designed from the ground + up to match GTK's widget model, including templates, child types, signal + handlers, and menus. +- **Easy to learn.** The syntax should be very familiar to most people. Take a look at the :doc:`reference ` to get started. +- **Modern tooling.** Blueprint ships a `Language Server `_ for IDE integration. + +Links +----- + +- `Source code `_ +- `Workbench `_ lets you try, preview and export Blueprint +- `GNOME Builder `_ provides builtin support +- `Vim syntax highlighting plugin by thetek42 `_ +- `Vim syntax highlighting plugin by gabmus `_ +- `GNU Emacs major mode by DrBluefall `_ +- `Visual Studio Code plugin by bodil `_ + +History +------- + +1. `Simplify our UI declarative language, a strawman proposal `_ +2. `A Markup Language for GTK `_ +3. `Introducing Blueprint: A New Way to Craft User Interfaces `_ +4. `Next Steps for Blueprint `_ + +Built with Blueprint +-------------------- + +- `AdwSteamGtk `_ +- `Blurble `_ +- `Bottles `_ +- `Cartridges `_ +- `Cassette `_ +- `Cavalier `_ +- `Chance `_ +- `Commit `_ +- `Confy `_ +- `Cozy `_ +- `Daikhan `_ +- `Damask `_ +- `Denaro `_ +- `Design `_ +- `Dev Toolbox `_ +- `Dialect `_ +- `Diccionario de la Lengua `_ +- `Doggo `_ +- `Dosage `_ +- `Dynamic Wallpaper `_ +- `Extension Manager `_ +- `Eyedropper `_ +- `favagtk `_ +- `Feeds `_ +- `File Shredder `_ +- `Flare `_ +- `Flowtime `_ +- `Fretboard `_ +- `Frog `_ +- `Geopard `_ +- `Giara `_ +- `Girens `_ +- `Gradience `_ +- `Graphs `_ +- `Health `_ +- `HydraPaper `_ +- `Identity `_ +- `Jogger `_ +- `Junction `_ +- `Komikku `_ +- `Letterpress `_ +- `Login Manager Settings `_ +- `Maniatic Launcher `_ +- `Master Key `_ +- `Misson Center `_ +- `NewCaw `_ +- `Paper `_ +- `Paper Plane `_ +- `Parabolic `_ +- `Passes `_ +- `Pipeline `_ +- `Playhouse `_ +- `Plitki `_ +- `Raider `_ +- `Retro `_ +- `Solanum `_ +- `Sudoku Solver `_ +- `Swatch `_ +- `Switcheroo `_ +- `Tagger `_ +- `Tangram `_ +- `Text Pieces `_ +- `Upscaler `_ +- `Video Trimmer `_ +- `Webfont Kit Generator `_ +- `WhatIP `_ +- `Who Wants To Be a Millionaire `_ +- `Workbench `_ \ No newline at end of file diff --git a/gtk/blueprint-compiler/docs/meson.build b/gtk/blueprint-compiler/docs/meson.build new file mode 100644 index 00000000000..d9ad7366c00 --- /dev/null +++ b/gtk/blueprint-compiler/docs/meson.build @@ -0,0 +1,19 @@ +if get_option('docs') + +sphinx = find_program(['sphinx-build-3', 'sphinx-build'], required: true) + +custom_target('docs', + command: [sphinx, '-b', 'html', '-c', meson.current_source_dir(), meson.current_source_dir(), '@OUTPUT@'], + output: 'en', + build_always_stale: true, +) + +endif + +custom_target('reference_docs.json', + output: 'reference_docs.json', + command: [meson.current_source_dir() / 'collect-sections.py', '@OUTPUT@'], + build_always_stale: true, + install: true, + install_dir: py.get_install_dir() / 'blueprintcompiler', +) \ No newline at end of file diff --git a/gtk/blueprint-compiler/docs/packaging.rst b/gtk/blueprint-compiler/docs/packaging.rst new file mode 100644 index 00000000000..78bb2c2bbca --- /dev/null +++ b/gtk/blueprint-compiler/docs/packaging.rst @@ -0,0 +1,36 @@ +==================== +For Distro Packagers +==================== + +blueprint-compiler is a build tool that converts UI definitions written in +Blueprint into XML files that are installed with the app and that GTK can read. +So for most applications that use blueprint-compiler, it is a build dependency. +It is a Python program, but like most GNOME-related projects, it uses +`Meson `_ as its build system. + +GObject Introspection +~~~~~~~~~~~~~~~~~~~~~ + +Blueprint files can import GObject Introspection namespaces like this: + +.. code-block:: blueprint + + using Gtk 4.0; + using Adw 1; + +To compile a blueprint file, ``.typelib`` files for all of the imported +namespaces must be installed. All blueprint files must import Gtk 4.0, so +``Gtk-4.0.typelib`` is effectively a runtime dependency of blueprint-compiler. +blueprint-compiler also depends on pygobject, because it uses GIRepository +to determine the search path for typelib files. + +So, if a package uses blueprint-compiler, its build dependencies should include +the typelib files for any namespaces imported in its blueprint files. (Note +that many apps also have the same typelib files as runtime dependencies, +separately from blueprint). + +In addition, the blueprint language server uses ``.gir`` files to provide +documentation on hover. Some distros package these files separately from the +main package (e.g. in a ``-devel`` package). The language server will not crash +if these files are not present, but for a good user experience you should make +sure they are installed. \ No newline at end of file diff --git a/gtk/blueprint-compiler/docs/reference/diagnostics.rst b/gtk/blueprint-compiler/docs/reference/diagnostics.rst new file mode 100644 index 00000000000..c8774d6a6d6 --- /dev/null +++ b/gtk/blueprint-compiler/docs/reference/diagnostics.rst @@ -0,0 +1,188 @@ +=========== +Diagnostics +=========== + + +.. _Diagnostic abstract_class: + +abstract_class +-------------- +Objects can't be created from abstract classes. Abstract classes are used as base classes for other classes, but they don't have functionality on their own. You may want to use a non-abstract subclass instead. + + +.. _Diagnostic bad_syntax: + +bad_syntax +---------- +The tokenizer encountered an unexpected sequence of characters that aren't part of any known blueprint syntax. + + +.. _Diagnostic child_not_accepted: + +child_not_accepted +------------------ +The parent class does not have child objects (it does not implement `Gtk.Buildable `_ and is not a subclass of `Gio.ListStore `_). Some classes use properties instead of children to add widgets. Check the parent class's documentation. + + +.. _Diagnostic conversion_error: + +conversion_error +---------------- +The value's type cannot be converted to the target type. + +Subclasses may be converted to their superclasses, but not vice versa. A type that implements an interface can be converted to that interface's type. Many boxed types can be parsed from strings in a type-specific way. + + +.. _Diagnostic expected_bool: + +expected_bool +------------- +A boolean value was expected, but the value is not ``true`` or ``false``. + + +.. _Diagnostic extension_not_repeatable: + +extension_not_repeatable +------------------------ +This extension can't be used more than once in an object. + + +.. _Diagnostic extension_wrong_parent_type: + +extension_wrong_parent_type +--------------------------- +No extension with the given name exists for this object's class (or, for a :ref:`child extension`, the parent class). + + +.. _Diagnostic invalid_number_literal: + +invalid_number_literal +---------------------- +The tokenizer encountered what it thought was a number, but it couldn't parse it as a number. + + +.. _Diagnostic member_dne: + +member_dne +---------- +The value is being interpreted as a member of an enum or flag type, but that type doesn't have a member with the given name. + + +.. _Diagnostic missing_gtk_declaration: + +missing_gtk_declaration +----------------------- +All blueprint files must start with a GTK declaration, e.g. ``using Gtk 4.0;``. + + +.. _Diagnostic multiple_templates: + +multiple_templates +------------------ +Only one :ref:`template` is allowed per blueprint file, but there are multiple. The template keyword indicates which object is the one being instantiated. + + +.. _Diagnostic namespace_not_found: + +namespace_not_found +-------------------- +The ``.typelib`` files for the given namespace could not be found. There are several possibilities: + +* There is a typo in the namespace name, e.g. ``Adwaita`` instead of ``Adw`` + +* The version number is incorrect, e.g. ``Adw 1.0`` instead of ``Adw 1``. The library's documentation will tell you the correct version number to use. + +* The packages for the library are not installed. On some distributions, the ``.typelib`` file is in a separate package from the main library, such as a ``-devel`` package. + +* There is an issue with the path to the typelib file. The ``GI_TYPELIB_PATH`` environment variable can be used to add additional paths to search. + + +.. _Diagnostic namespace_not_imported: + +namespace_not_imported +---------------------- +The given namespace was not imported at the top of the file. Importing the namespace is necessary because it tells blueprint-compiler which version of the library to use. + + +.. _Diagnostic object_dne: + +object_dne +---------- +No object with the given ID exists in the current scope. + + +.. _Diagnostic property_dne: + +property_dne +------------ +The class or interface doesn't have a property with the given name. + + +.. _Diagnostic property_convert_error: + +property_convert_error +---------------------- +The value given for the property can't be converted to the property's type. + + +.. _Diagnostic property_construct_only: + +property_construct_only +----------------------- +The property can't be bound because it is a construct-only property, meaning it can only be set once when the object is first constructed. Binding it to an expression could cause its value to change later. + + +.. _Diagnostic property_read_only: + +property_read_only +------------------ +This property can't be set because it is marked as read-only. + + +.. _Diagnostic signal_dne: + +signal_dne +---------- +The class or interface doesn't have a signal with the given name. + + +.. _Diagnostic type_dne: + +type_dne +-------- +The given type doesn't exist in the namespace. + + +.. _Diagnostic type_not_a_class: + +type_not_a_class +---------------- +The given type exists in the namespace, but it isn't a class. An object's type must be a concrete (not abstract) class, not an interface or boxed type. + + +.. _Diagnostic version_conflict: + +version_conflict +---------------- +This error occurs when two versions of a namespace are imported (possibly transitively) in the same file. For example, this will cause a version conflict: + +.. code-block:: blueprint + + using Gtk 4.0; + using Gtk 3.0; + +But so will this: + +.. code-block:: blueprint + + using Gtk 4.0; + using Handy 1; + +because libhandy imports ``Gtk 3.0``. + + +.. _Diagnostic wrong_compiler_version: + +wrong_compiler_version +---------------------- +This version of blueprint-compiler is for GTK 4 blueprints only. Future GTK versions will use different versions of blueprint-compiler. diff --git a/gtk/blueprint-compiler/docs/reference/document_root.rst b/gtk/blueprint-compiler/docs/reference/document_root.rst new file mode 100644 index 00000000000..ee97d6a38bd --- /dev/null +++ b/gtk/blueprint-compiler/docs/reference/document_root.rst @@ -0,0 +1,88 @@ +======================= +Document Root & Imports +======================= + + +.. _Syntax Root: + +Document Root +------------- + +.. rst-class:: grammar-block + + Root = :ref:`GtkDecl` (:ref:`Using`)* (:ref:`TranslationDomain`)? ( :ref:`Template` | :ref:`Menu` | :ref:`Object` )* EOF + +A blueprint document consists of a :ref:`GTK declaration`, one or more :ref:`imports`, and a list of :ref:`objects` and/or a :ref:`template`. + +Example +~~~~~~~ + +.. code-block:: blueprint + + // Gtk Declaration + using Gtk 4.0; + + // Import Statement + using Adw 1; + + // Object + Window my_window {} + + +.. _Syntax GtkDecl: + +GTK Declaration +--------------- + +.. rst-class:: grammar-block + + GtkDecl = 'using' 'Gtk' '4.0' ';' + +Every blueprint file begins with the line ``using Gtk 4.0;``, which declares the target GTK version for the file. Tools that read blueprint files should verify that they support the declared version. + +Example +~~~~~~~ + +.. code-block:: blueprint + + using Gtk 4.0; + + +.. _Syntax Using: + +GObject Introspection Imports +----------------------------- + +.. rst-class:: grammar-block + + Using = 'using' `> `> ';' + +To use classes and types from namespaces other than GTK itself, those namespaces must be imported at the top of the file. This tells the compiler what version of the namespace to import. + +You'll need the GIR name and version, not the package name and not the exact version number. These are listed at the top of each library's documentation homepage: + +.. image:: gir-namespace.png + +The compiler requires typelib files for these libraries to be installed. They are usually installed with the library, but on some distros, you may need to install the package that provides ``{namespace}-{version}.typelib`` (e.g. ``Adw-1.typelib``). + +Example +~~~~~~~ + +.. code-block:: blueprint + + // Import libadwaita + using Adw 1; + + +.. _Syntax TranslationDomain: + +Translation Domain +------------------ + +.. rst-class:: grammar-block + + TranslationDomain = 'translation-domain' `> ';' + +The translation domain is used to look up translations for translatable strings in the blueprint file. If no translation domain is specified, strings will be looked up in the program's global domain. + +See `Gtk.Builder:translation-domain `_ for more information. diff --git a/gtk/blueprint-compiler/docs/reference/expressions.rst b/gtk/blueprint-compiler/docs/reference/expressions.rst new file mode 100644 index 00000000000..8688ff07b27 --- /dev/null +++ b/gtk/blueprint-compiler/docs/reference/expressions.rst @@ -0,0 +1,87 @@ +=========== +Expressions +=========== + +Expressions make your user interface code *reactive*. This means when your +application's data changes, the user interface reacts to the change +automatically. + +.. code-block:: blueprint + + label: bind template.account.username; + /* ^ ^ ^ + | creates lookup expressions that are re-evaluated when + | the account's username *or* the account itself changes + | + binds the `label` property to the expression's output + */ + +When a value is bound to an expression using the ``bind`` keyword, the binding +monitors all the object properties that are inputs to the expression, and +reevaluates it if any of them change. + +This is a powerful tool for ensuring consistency and simplifying your code. +Rather than pushing changes to the user interface wherever they may occur, +you can define your data model with GObject and let GTK take care of the rest. + +.. _Syntax Expression: + +Expressions +----------- + +.. rst-class:: grammar-block + + Expression = ( :ref:`ClosureExpression` | :ref:`Literal` | ( '(' Expression ')' ) ) ( :ref:`LookupExpression` | :ref:`CastExpression` )* + +.. note:: + + The grammar above is designed to eliminate `left recursion `_, which can make parsing more complex. In this format, an expression consists of a prefix (such as a literal value or closure invocation) followed by zero or more infix or suffix operators. + +Expressions are composed of property lookups and/or closures. Property lookups are the inputs to the expression, and closures provided in application code can perform additional calculations on those inputs. + + +.. _Syntax LookupExpression: + +Lookup Expressions +------------------ + +.. rst-class:: grammar-block + + LookupExpression = '.' `> + +Lookup expressions perform a GObject property lookup on the preceding expression. They are recalculated whenever the property changes, using the `notify signal `_. + +The type of a property expression is the type of the property it refers to. + + +.. _Syntax ClosureExpression: + +Closure Expressions +------------------- + +.. rst-class:: grammar-block + + ClosureExpression = '$' `> '(' ( :ref:`Expression` ),* ')' + +Closure expressions allow you to perform additional calculations that aren't supported in blueprint by writing those calculations as application code. These application-defined functions are created in the same way as :ref:`signal handlers`. + +Expressions are only reevaluated when their inputs change. Because blueprint doesn't manage a closure's application code, it can't tell what changes might affect the result. Therefore, closures must be *pure*, or deterministic. They may only calculate the result based on their immediate inputs, not properties of their inputs or outside variables. + +Blueprint doesn't know the closure's return type, so closure expressions must be cast to the correct return type using a :ref:`cast expression`. + + +.. _Syntax CastExpression: + +Cast Expressions +---------------- + +.. rst-class:: grammar-block + + CastExpression = 'as' '<' :ref:`TypeName` '>' + +Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. This is necessary for closures and for properties of application-defined types. + +.. code-block:: blueprint + + // Cast the result of the closure so blueprint knows it's a string + label: bind $my_closure() as \ No newline at end of file diff --git a/gtk/blueprint-compiler/docs/reference/extensions.rst b/gtk/blueprint-compiler/docs/reference/extensions.rst new file mode 100644 index 00000000000..0961d144f36 --- /dev/null +++ b/gtk/blueprint-compiler/docs/reference/extensions.rst @@ -0,0 +1,374 @@ +========== +Extensions +========== + +.. _Syntax Extension: + +Properties are the main way to set values on objects, but they are limited by the GObject type system in what values they can accept. Some classes, therefore, have specialized syntax for certain features. + +.. note:: + + Extensions are a feature of ``Gtk.Buildable``--see `Gtk.Buildable.custom_tag_start() `_ for internal details. + + Because they aren't part of the type system, they aren't present in typelib files like properties and signals are. Therefore, if a library adds a new extension, syntax for it must be added to Blueprint manually. If there's a commonly used extension that isn't supported by Blueprint, please `file an issue `_. + +.. rst-class:: grammar-block + + Extension = :ref:`ExtAccessibility` + | :ref:`ExtAdwAlertDialog` + | :ref:`ExtAdwMessageDialog` + | :ref:`ExtAdwBreakpoint` + | :ref:`ExtComboBoxItems` + | :ref:`ExtFileFilterMimeTypes` + | :ref:`ExtFileFilterPatterns` + | :ref:`ExtFileFilterSuffixes` + | :ref:`ExtLayout` + | :ref:`ExtListItemFactory` + | :ref:`ExtSizeGroupWidgets` + | :ref:`ExtStringListStrings` + | :ref:`ExtStyles` + + +.. _Syntax ExtAccessibility: + +Accessibility Properties +------------------------ + +.. rst-class:: grammar-block + + ExtAccessibility = 'accessibility' '{' ExtAccessibilityProp* '}' + ExtAccessibilityProp = `> ':' (:ref:`Value ` | ('[' (:ref: Value ),* ']') ) ';' + +Valid in any `Gtk.Widget `_. + +The ``accessibility`` block defines values relevant to accessibility software. The property names and acceptable values are described in the `Gtk.AccessibleRelation `_, `Gtk.AccessibleState `_, and `Gtk.AccessibleProperty `_ enums. + +.. note:: + + Relations which allow for a list of values, for example `labelled-by`, must be given as a single relation with a list of values instead of duplicating the relation like done in Gtk.Builder. + +.. _Syntax ExtAdwBreakpoint: + +Adw.Breakpoint +-------------- + +.. rst-class:: grammar-block + + ExtAdwBreakpointCondition = 'condition' '(' `> ')' + ExtAdwBreakpoint = 'setters' '{' ExtAdwBreakpointSetter* '}' + ExtAdwBreakpointSetter = `> '.' `> ':' :ref:`Value ` ';' + +Valid in `Adw.Breakpoint `_. + +Defines the condition for a breakpoint and the properties that will be set at that breakpoint. See the documentation for `Adw.Breakpoint `_. + +.. note:: + + The `Adw.Breakpoint:condition `_ property has type `Adw.BreakpointCondition `_, which GtkBuilder doesn't know how to parse from a string. Therefore, the ``condition`` syntax is used instead. + + +.. _Syntax ExtAdwAlertDialog: + +Adw.AlertDialog Responses +---------------------------- + +.. rst-class:: grammar-block + + ExtAdwAlertDialog = 'responses' '[' (ExtAdwAlertDialogResponse),* ']' + ExtAdwAlertDialogResponse = `> ':' :ref:`StringValue` ExtAdwAlertDialogFlag* + ExtAdwAlertDialogFlag = 'destructive' | 'suggested' | 'disabled' + +Valid in `Adw.AlertDialog `_. + +The ``responses`` block defines the buttons that will be added to the dialog. The ``destructive`` or ``suggested`` flag sets the appearance of the button, and the ``disabled`` flag can be used to disable the button. + +.. code-block:: blueprint + + using Adw 1; + + Adw.AlertDialog { + responses [ + cancel: _("Cancel"), + delete: _("Delete") destructive, + save: "Save" suggested, + wipeHardDrive: "Wipe Hard Drive" destructive disabled, + ] + } + + +.. _Syntax ExtAdwMessageDialog: + +Adw.MessageDialog Responses +---------------------------- + +.. rst-class:: grammar-block + + ExtAdwMessageDialog = 'responses' '[' (ExtAdwMessageDialogResponse),* ']' + ExtAdwMessageDialogResponse = `> ':' :ref:`StringValue` ExtAdwMessageDialogFlag* + ExtAdwMessageDialogFlag = 'destructive' | 'suggested' | 'disabled' + +Valid in `Adw.MessageDialog `_. + +The ``responses`` block defines the buttons that will be added to the dialog. The ``destructive`` or ``suggested`` flag sets the appearance of the button, and the ``disabled`` flag can be used to disable the button. + +.. code-block:: blueprint + + using Adw 1; + + Adw.MessageDialog { + responses [ + cancel: _("Cancel"), + delete: _("Delete") destructive, + save: "Save" suggested, + wipeHardDrive: "Wipe Hard Drive" destructive disabled, + ] + } + + +.. _Syntax ExtComboBoxItems: + +Gtk.ComboBoxText Items +---------------------- + +.. rst-class:: grammar-block + + ExtComboBoxItems = 'items' '[' (ExtComboBoxItem),* ']' + ExtComboBoxItem = ( `> ':' )? :ref:`StringValue` + +Valid in `Gtk.ComboBoxText `_, which is deprecated as of Gtk 4.10. + +The ``items`` block defines the items that will be added to the combo box. The optional ID can be used to refer to the item rather than its label. + +.. code-block:: blueprint + + ComboBoxText { + items [ + item1: "Item 1", + item2: "Item 2", + item3: "Item 3", + ] + } + + +.. _Syntax ExtFileFilter: + +Gtk.FileFilter Filters +---------------------- + +.. rst-class:: grammar-block + + ExtFileFilterMimeTypes = 'mime-types' '[' (ExtFileFilterItem),* ']' + ExtFileFilterPatterns = 'patterns' '[' (ExtFileFilterItem),* ']' + ExtFileFilterSuffixes = 'suffixes' '[' (ExtFileFilterItem),* ']' + ExtFileFilterItem = `> + +Valid in `Gtk.FileFilter `_. + +The ``mime-types``, ``patterns``, and ``suffixes`` blocks define the items that will be added to the file filter. The ``mime-types`` block accepts mime types (including wildcards for subtypes, such as ``image/*``). The ``patterns`` block accepts glob patterns, and the ``suffixes`` block accepts file extensions. + +.. code-block:: blueprint + + FileFilter { + mime-types [ "text/plain", "image/*" ] + patterns [ "*.txt" ] + suffixes [ "png", "jpg" ] + } + + +.. _Syntax ExtLayout: + +Widget Layouts +-------------- + +.. rst-class:: grammar-block + + ExtLayout = 'layout' '{' ExtLayoutProp* '}' + ExtLayoutProp = `> ':' :ref:`Value` ';' + +Valid in `Gtk.Widget `_. + +The ``layout`` block describes how the widget should be positioned within its parent. The available properties depend on the parent widget's layout manager. + +.. code-block:: blueprint + + Grid { + Button { + layout { + column: 0; + row: 0; + } + } + Button { + layout { + column: 1; + row: 0; + } + } + Button { + layout { + column: 0; + row: 1; + row-span: 2; + } + } + } + + +.. _Syntax ExtListItemFactory: + +Gtk.BuilderListItemFactory Templates +------------------------------------ + +.. rst-class:: grammar-block + + ExtListItemFactory = 'template' :ref:`TypeName` :ref:`ObjectContent` + +Valid in `Gtk.BuilderListItemFactory `_. + +The ``template`` block defines the template that will be used to create list items. This block is unique within Blueprint because it defines a completely separate sub-blueprint which is used to create each list item. The sub-blueprint may not reference objects in the main blueprint or vice versa. + +The template type must be `Gtk.ListItem `_, `Gtk.ColumnViewRow `_, or `Gtk.ColumnViewCell `_. The template object can be referenced with the ``template`` keyword. + +.. code-block:: blueprint + + ListView { + factory: BuilderListItemFactory { + template ListItem { + child: Label { + label: bind template.item as .string; + }; + } + }; + + model: NoSelection { + model: StringList { + strings [ "Item 1", "Item 2", "Item 3" ] + }; + }; + } + + +.. _Syntax ExtScaleMarks: + +Gtk.Scale Marks +--------------- + +.. rst-class:: grammar-block + + ExtScaleMarks = 'marks' '[' (ExtScaleMark),* ']' + ExtScaleMark = 'mark' '(' ( '-' | '+' )? `> ( ',' `> ( ',' :ref:`StringValue` )? )? ')' + +Valid in `Gtk.Scale `_. + +The ``marks`` block defines the marks on a scale. A single ``mark`` has up to three arguments: a value, an optional position, and an optional label. The position can be ``left``, ``right``, ``top``, or ``bottom``. The label may be translated. + + +.. _Syntax ExtSizeGroupWidgets: + +Gtk.SizeGroup Widgets +--------------------- + +.. rst-class:: grammar-block + + ExtSizeGroupWidgets = 'widgets' '[' (ExtSizeGroupWidget),* ']' + ExtSizeGroupWidget = `> + +Valid in `Gtk.SizeGroup `_. + +The ``widgets`` block defines the widgets that will be added to the size group. + +.. code-block:: blueprint + + Box { + Button button1 {} + Button button2 {} + } + + SizeGroup { + widgets [button1, button2] + } + + +.. _Syntax ExtStringListStrings: + +Gtk.StringList Strings +---------------------- + +.. rst-class:: grammar-block + + ExtStringListStrings = 'strings' '[' (ExtStringListItem),* ']' + ExtStringListItem = :ref:`StringValue` + +Valid in `Gtk.StringList `_. + +The ``strings`` block defines the strings in the string list. + +.. code-block:: blueprint + + StringList { + strings ["violin", "guitar", _("harp")] + } + + +.. _Syntax ExtStyles: + +CSS Styles +---------- + +.. rst-class:: grammar-block + + ExtStyles = 'styles' '[' ExtStylesProp* ']' + ExtStylesProp = `> + +Valid in any `Gtk.Widget `_. + +The ``styles`` block defines CSS classes that will be added to the widget. + +.. code-block:: blueprint + + Button { + styles ["suggested-action"] + } + + +.. _Syntax ChildExtension: + +Child Extensions +---------------- + +.. rst-class:: grammar-block + + ChildExtension = :ref:`ExtResponse` + +Child extensions are similar to regular extensions, but they apply to a child of the object rather than the object itself. They are used to add properties to child widgets of a container, such as the buttons in a `Gtk.Dialog `_. The child extension takes the place of a child type inside the square brackets. + +Currently, the only child extension is :ref:`ExtResponse`. + + +.. _Syntax ExtResponse: + +Dialog & InfoBar Responses +-------------------------- + +.. rst-class:: grammar-block + + ExtResponse = 'action' 'response' '=' ( `> | `> ) 'default'? + +Valid as a child extension for children of `Gtk.Dialog `_ or `Gtk.InfoBar `_, which are both deprecated as of Gtk 4.10. + +The ``action response`` extension sets the ``action`` child type for the child and sets the child's integer response type. The response type may be either a member of the `Gtk.ResponseType `_ enum or a positive, application-defined integer. + +No more than one child of a dialog or infobar may have the ``default`` flag. + +.. code-block:: blueprint + + Dialog { + [action response=ok default] + Button {} + + [action response=cancel] + Button {} + + [action response=1] + Button {} + } diff --git a/gtk/blueprint-compiler/docs/reference/gir-namespace.png b/gtk/blueprint-compiler/docs/reference/gir-namespace.png new file mode 100644 index 00000000000..05eaca0472f Binary files /dev/null and b/gtk/blueprint-compiler/docs/reference/gir-namespace.png differ diff --git a/gtk/blueprint-compiler/docs/reference/index.rst b/gtk/blueprint-compiler/docs/reference/index.rst new file mode 100644 index 00000000000..d49feb96e9b --- /dev/null +++ b/gtk/blueprint-compiler/docs/reference/index.rst @@ -0,0 +1,50 @@ +================ +Syntax Reference +================ + +This is the official specification of the blueprint format. + +The grammar is expressed as a `parsing expression grammar `_. This has two important implications: the parser will never backtrack, and alternation (e.g. a ``|`` in the specification) will always take the *first* branch that matches, even if that causes an error later. These properties make PEGs both unambiguous and simple to implement in code. + +Blueprint uses C-style line comments (``// comment for the rest of the line``) and block comments (``/* multiline comment... */``). + +Wherever commas are used as delimiters in repetition (expressed in this reference as ``( ),*``), the trailing comma is permitted and optional. + +.. toctree:: + :maxdepth: 1 + + document_root + objects + templates + values + expressions + menus + extensions + diagnostics + + +Tokens +------ + +.. _Syntax IDENT: + +IDENT +~~~~~ + +An identifier starts with an ASCII underscore ``_`` or letter ``[A-Za-z]`` and consists of ASCII underscores, letters, digits ``[0-9]``, and dashes ``-``. Dashes are included for historical reasons, since GObject properties and signals are traditionally kebab-case. + +.. _Syntax NUMBER: + +NUMBER +~~~~~~ + +Numbers begin with an ASCII digit and consist of ASCII digits, underscores, dots ``.``, and letters (for radix pre-/suffixes). More than one dot in a number is not allowed. Underscores are permitted for increased readability, and are ignored. + +Hexadecimal numbers may be specified using the ``0x`` prefix and may use uppercase or lowercase letters, or a mix. Hexadecimal values may not have a fractional part. They are generally converted to decimal in the output. + +.. _Syntax QUOTED: + +QUOTED +~~~~~~ + +Quotes begin with an ASCII single quote ``'`` or double quote ``"`` and end with the same character they started with. An ASCII backslash ``\`` begins an escape sequence; this allows newlines ``\n``, tabs ``\t``, and quotes ``\'``, ``\"`` to be inserted. It also allows multiline strings by escaping a newline character, which will be ignored. diff --git a/gtk/blueprint-compiler/docs/reference/menus.rst b/gtk/blueprint-compiler/docs/reference/menus.rst new file mode 100644 index 00000000000..2d7bfeaa781 --- /dev/null +++ b/gtk/blueprint-compiler/docs/reference/menus.rst @@ -0,0 +1,62 @@ +===== +Menus +===== + +.. _Syntax Menu: + +Menus +----- + +.. rst-class:: grammar-block + + Menu = 'menu' `>? '{' MenuChild* '}' + MenuChild = ( MenuSection | MenuSubmenu | :ref:`MenuItemShorthand` | MenuItem ) + MenuSection = 'section' `>? '{' ( MenuChild | MenuAttribute )* '}' + MenuSubmenu = 'submenu' `>? '{' ( MenuChild | MenuAttribute )* '}' + MenuItem = 'item' '{' MenuAttribute* '}' + MenuAttribute = `> ':' :ref:`StringValue` ';' + +Menus, such as the application menu, are defined using the ``menu`` keyword. Menus have the type `Gio.MenuModel `_ and can be referenced by ID. They cannot be defined inline. + +Example +~~~~~~~ + +.. code-block:: blueprint + + menu my_menu { + submenu { + label: _("File"); + item { + label: _("New"); + action: "app.new"; + icon: "document-new-symbolic"; + } + } + } + + MenuButton { + menu-model: my_menu; + } + + +.. _Syntax MenuItemShorthand: + +Item Shorthand +-------------- + +.. rst-class:: grammar-block + + MenuItemShorthand = 'item' '(' :ref:`StringValue` ( ',' ( :ref:`StringValue` ( ',' :ref:`StringValue`? )? )? )? ')' + +The most common menu attributes are ``label``, ``action``, and ``icon``. Because they're so common, Blueprint provides a shorter syntax for menu items with just these properties. + +Example +~~~~~~~ + +.. code-block:: blueprint + + menu { + item ("label") + item ("label", "action") + item ("label", "action", "icon") + } diff --git a/gtk/blueprint-compiler/docs/reference/objects.rst b/gtk/blueprint-compiler/docs/reference/objects.rst new file mode 100644 index 00000000000..699db498a1c --- /dev/null +++ b/gtk/blueprint-compiler/docs/reference/objects.rst @@ -0,0 +1,189 @@ +======= +Objects +======= + + +.. _Syntax Object: + +Objects +------- + +.. rst-class:: grammar-block + + Object = :ref:`TypeName` `>? ObjectContent + ObjectContent = '{' (:ref:`Signal` | :ref:`Property` | :ref:`Extension` | :ref:`Child`)* '}' + +Objects are the basic building blocks of a GTK user interface. Your widgets are all objects, as are some other features such as list models. + +Optionally, objects may have an ID to provide a handle for other parts of the blueprint and your code to access objects. + +.. note:: + + Object IDs must be unique within their scope. The document root is a scope, but :ref:`sub-templates` have their own, isolated scope. + +Example +~~~~~~~ + +.. code-block:: blueprint + + Label label1 { + label: "Hello, world!"; + } + Label label2 { + label: bind-property file.name; + } + + +.. _Syntax TypeName: + +Type Names +---------- + +.. rst-class:: grammar-block + + TypeName = TypeNameFull | TypeNameExternal | TypeNameShort + TypeNameFull = `> '.' `> + TypeNameExternal = '$' `> + TypeNameShort = `> + +There are three forms of type names: full, short, and external. Full type names take the form ``{namespace}.{name}``, e.g. ``Gtk.ApplicationWindow`` or ``Adw.Leaflet``. Because GTK types are so common, the Gtk namespace may be omitted, shortening ``Gtk.ApplicationWindow`` to just ``ApplicationWindow``. + +External type names refer to types defined in your application. They are prefixed with ``$`` and do not have a dot between the namespace and class name. In fact, anywhere a ``$`` is used in a blueprint, it refers to something that must be defined in your application code. + + +.. _Syntax Property: + +Properties +---------- + +.. rst-class:: grammar-block + + Property = `> ':' ( :ref:`Binding` | :ref:`ObjectValue` | :ref:`Value` ) ';' + +Properties specify the details of each object, like a label's text, an image's icon name, or the margins on a container. + +Most properties are static and directly specified in the blueprint, but properties can also be bound to a data model using the ``bind`` or ``bind-property`` keywords. + +A property's value can be another object, either inline or referenced by ID. + +Example +~~~~~~~ + +.. code-block:: blueprint + + Label { + label: "text"; + } + + Button { + /* Inline object value. Notice the semicolon after the object. */ + child: Image { + /* ... */ + }; + } + + +.. _Syntax Signal: + +Signal Handlers +--------------- + +.. rst-class:: grammar-block + + Signal = `> ('::' `>)? '=>' '$' `> '(' `>? ')' (SignalFlag)* ';' + SignalFlag = 'after' | 'swapped' + +Signals are one way to respond to user input (another is `actions `_, which use the `action-name property `_). + +Signals provide a handle for your code to listen to events in the UI. The handler name is prefixed with ``$`` to indicate that it's an external symbol which needs to be provided by your code; if it isn't, things might not work correctly, or at all. + +Optionally, you can provide an object ID to use when connecting the signal. + +Example +~~~~~~~ + +.. code-block:: blueprint + + Button { + clicked => $on_button_clicked(); + } + + +.. _Syntax Child: + +Children +-------- + +.. rst-class:: grammar-block + + Child = ChildAnnotation? :ref:`Object` + ChildAnnotation = '[' ( ChildInternal | :ref:`ChildExtension` | ChildType ) ']' + ChildInternal = 'internal-child' `> + ChildType = `> + +Some objects can have children. This defines the hierarchical structure of a user interface: containers contain widgets, which can be other containers, and so on. + +Child annotations are defined by the parent widget. Some widgets, such as `HeaderBar `_, have "child types" which allow different child objects to be treated in different ways. Some, such as `Dialog `_ and `InfoBar `_, define child :ref:`extensions`, which provide more detailed information about the child. + +Internal children are a special case. Rather than creating a new object, children marked with ``[internal-child ]`` modify an existing object provided by the parent. This is used, for example, for the ``content_area`` of a `Dialog `_. + +.. note:: + + The objects at the root of a blueprint cannot have child annotations, since there is no root widget for them to be a child of. + +.. note:: + + Some widgets, like `Button `_, use a property to set their child instead. Widgets added in this way don't have child annotations. + +Examples +~~~~~~~~ + +Add children to a container ++++++++++++++++++++++++++++ + +.. code-block:: blueprint + + Button { + Image {} + } + +Child types ++++++++++++ + +.. code-block:: blueprint + + HeaderBar { + [start] + Label { + } + + [end] + Button { + } + } + +Child extensions +++++++++++++++++ + +.. code-block:: blueprint + + Dialog { + // Here, a child extension annotation defines the button's response. + [action response=cancel] + Button {} + } + +Internal children ++++++++++++++++++ + +.. code-block:: blueprint + + Dialog { + [internal-child content_area] + Box { + // Unlike most objects in a blueprint, this internal-child widget + // represents the properties, signal handlers, children, and extensions + // of an existing Box created by the Dialog, not a new Box created by + // the blueprint. + } + } diff --git a/gtk/blueprint-compiler/docs/reference/templates.rst b/gtk/blueprint-compiler/docs/reference/templates.rst new file mode 100644 index 00000000000..74e4225c026 --- /dev/null +++ b/gtk/blueprint-compiler/docs/reference/templates.rst @@ -0,0 +1,96 @@ +=================== +Composite Templates +=================== + +.. _Syntax Template: + +Composite Templates +------------------- + +.. rst-class:: grammar-block + + Template = 'template' :ref:`TypeName` ( ':' :ref:`TypeName` )? :ref:`ObjectContent` + +Widget subclassing is one of the primary techniques for structuring an application. For example, a maps app might have a `Gtk.ApplicationWindow `_ subclass, ``MapsApplicationWindow``, that implements the functionality of its main window. But a maps app has a lot of functionality, so the headerbar might be split into its own `Gtk.HeaderBar `_ subclass, ``MapsHeaderBar``, for the sake of organization. + +You could implement this with the following blueprint: + +.. code-block:: blueprint + + using Gtk 4.0; + + $MapsApplicationWindow window { + $MapsHeaderBar { + /* probably a lot of buttons ... */ + } + + $MapsMainView { + /* a lot more UI definitions ... */ + } + } + +There are two problems with this approach: + +1. The widget code may be organized neatly into different files, but the UI is not. This blueprint contains the entire UI definition for the app. + +2. Widgets aren't in control of their own contents. It shouldn't be up to the caller to construct a widget using the correct blueprint--that's an implementation detail of the widget. + +We can solve these problems by giving each widget its own blueprint file, which we reference in the widget's constructor. Then, whenever the widget is instantiated (by another blueprint, or by the application), it will get all the children and properties defined in its blueprint. + +For this to work, we need to specify in the blueprint which object is the one being instantiated. We do this with a template block: + +.. code-block:: blueprint + + using Gtk 4.0; + + template $MapsHeaderBar : Gtk.HeaderBar { + /* probably a lot of buttons ... */ + } + + Gio.ListStore bookmarked_places_store { + /* This isn't the object being instantiated, just an auxillary object. GTK knows this because it isn't the + one marked with 'template'. */ + } + +This blueprint can only be used by the ``MapsHeaderBar`` constructor. Instantiating it with ``Gtk.Builder`` won't work since it needs an existing, under-construction ``MapsHeaderBar`` to use for the template object. The ``template`` block must be at the top level of the file (not nested within another object) and only one is allowed per file. + +This ``MapsHeaderBar`` class, along with its blueprint template, can then be referenced in another blueprint: + +.. code-block:: blueprint + + using Gtk 4.0; + + ApplicationWindow { + $MapsHeaderBar { + /* Nothing needed here, the widgets are in the MapsHeaderBar template. */ + } + } + +Type & Parent Parameters +~~~~~~~~~~~~~~~~~~~~~~~~ + +The type name that directly follows the ``template`` keyword is the type of the template class. In most cases, this will be an extern type starting with ``$`` and matching the class name in the application code. Templates for use in a `Gtk.BuilderListItemFactory `_ use ``ListItem`` as the type name instead. + +The parent type is optional, and may only be present if the template type is extern. It enables limited type checking for the properties and signals of the template object. + + +Referencing a Template +---------------------- + +To reference the template object in a binding or expression, use the ``template`` keyword: + +.. code-block:: blueprint + + template $MyTemplate { + prop1: "Hello, world!"; + prop2: bind template.prop1; + } + + +Language Implementations +------------------------ + +- **C** ``gtk_widget_class_set_template ()``: https://docs.gtk.org/gtk4/class.Widget.html#building-composite-widgets-from-template-xml +- **gtk-rs** ``#[template]``: https://gtk-rs.org/gtk4-rs/stable/latest/book/composite_templates.html +- **GJS** ``GObject.registerClass()``: https://gjs.guide/guides/gtk/3/14-templates.html +- **PyGObject** ``@Gtk.Template``: https://pygobject.gnome.org/guide/gtk_template.html diff --git a/gtk/blueprint-compiler/docs/reference/values.rst b/gtk/blueprint-compiler/docs/reference/values.rst new file mode 100644 index 00000000000..fd414a8536d --- /dev/null +++ b/gtk/blueprint-compiler/docs/reference/values.rst @@ -0,0 +1,181 @@ +====== +Values +====== + + +.. _Syntax Value: + +Values +------ + +.. rst-class:: grammar-block + + Value = :ref:`Translated` | :ref:`Flags` | :ref:`Literal` + + +.. _Syntax Literal: + +Literals +-------- + +.. rst-class:: grammar-block + + Literal = :ref:`TypeLiteral` | QuotedLiteral | NumberLiteral | IdentLiteral + QuotedLiteral = `> + NumberLiteral = ( '-' | '+' )? `> + IdentLiteral = `> + +Literals are used to specify values for properties. They can be strings, numbers, references to objects, ``null``, types, boolean values, or enum members. + +.. _Syntax TypeLiteral: + +Type Literals +------------- + +.. rst-class:: grammar-block + + TypeLiteral = 'typeof' '<' :ref:`TypeName` '>' + +Sometimes, you need to specify a type as a value. For example, when creating a list store, you may need to specify the type of the items in the list store. This is done using a ``typeof<>`` literal. + +The type of a ``typeof<>`` literal is `GType `_, GObject's "meta-type" for type information. + + +Example +~~~~~~~ + +.. code-block:: blueprint + + Gio.ListStore { + item-type: typeof; + } + + +.. _Syntax Flags: + +Flags +----- + +.. rst-class:: grammar-block + + Flags = `> '|' ( `> )|+ + +Flags are used to specify a set of options. One or more of the available flag values may be specified, and they are combined using ``|``. + +Example +~~~~~~~ + +.. code-block:: blueprint + + Adw.TabView { + shortcuts: control_tab | control_shift_tab; + } + + +.. _Syntax Translated: + +Translated Strings +------------------ + +.. rst-class:: grammar-block + + Translated = ( '_' '(' `> ')' ) | ( '\C_' '(' `> ',' `> ')' ) + + +Use ``_("...")`` to mark strings as translatable. You can put a comment for translators on the line above if needed. + +.. code-block:: blueprint + + Gtk.Label label { + /* Translators: This is the main text of the welcome screen */ + label: _("Hello, world!"); + } + +Use ``C_("context", "...")`` to add a *message context* to a string to disambiguate it, in case the same string appears in different places. Remember, two strings might be the same in one language but different in another depending on context. + +.. code-block:: blueprint + + Gtk.Label label { + /* Translators: This is a section in the preferences window */ + label: C_("preferences window", "Hello, world!"); + } + + +.. _Syntax Binding: + +Bindings +-------- + +.. rst-class:: grammar-block + + Binding = 'bind' :ref:`Expression` (BindingFlag)* + BindingFlag = 'inverted' | 'bidirectional' | 'no-sync-create' + +Bindings keep a property updated as other properties change. They can be used to keep the UI in sync with application data, or to connect two parts of the UI. + +The simplest bindings connect to a property of another object in the blueprint. When that other property changes, the bound property updates as well. More advanced bindings can do multi-step property lookups and can even call application code to compute values. See :ref:`the expressions page`. + +Simple Bindings +~~~~~~~~~~~~~~~ + +A binding that consists of a source object and a single lookup is called a "simple binding". These are implemented using `GObject property bindings `_ and support a few flags: + +- ``inverted``: For boolean properties, the target is set to the inverse of the source property. +- ``bidirectional``: The binding is two-way, so changes to the target property will also update the source property. +- ``no-sync-create``: Normally, when a binding is created, the target property is immediately updated with the current value of the source property. This flag disables that behavior, and the bound property will be updated the next time the source property changes. + +Complex Bindings +~~~~~~~~~~~~~~~~ + +Bindings with more complex expressions are implemented with `Gtk.Expression `_. These bindings do not support flags. + +Example +~~~~~~~ + +.. code-block:: blueprint + + /* Use bindings to show a label when a switch + * is active, without any application code */ + + Switch show_label {} + + Label { + visible: bind show_label.active; + label: _("I'm a label that's only visible when the switch is enabled!"); + } + +.. _Syntax ObjectValue: + +Object Values +------------- + +.. rst-class:: grammar-block + + ObjectValue = :ref:`Object` + +The value of a property can be an object, specified inline. This is particularly useful for widgets that use a ``child`` property rather than a list of child widgets. Objects constructed in this way can even have IDs and be referenced in other places in the blueprint. + +Such objects cannot have child annotations because they aren't, as far as blueprint is concerned, children of another object. + + +.. _Syntax StringValue: + +String Values +------------- + +.. rst-class:: grammar-block + + StringValue = :ref:`Translated` | :ref:`QuotedLiteral` + +Menus, as well as some :ref:`extensions`, have properties that can only be string literals or translated strings. + +.. _Syntax ArrayValue: + +Array Values +------------- + +.. rst-class:: grammar-block + + ArrayValue = '[' (:ref:`StringValue`),* ']' + +For now, it only supports :ref:`Strings`. This is because Gtk.Builder only supports string arrays. diff --git a/gtk/blueprint-compiler/docs/setup.rst b/gtk/blueprint-compiler/docs/setup.rst new file mode 100644 index 00000000000..839f8f6e402 --- /dev/null +++ b/gtk/blueprint-compiler/docs/setup.rst @@ -0,0 +1,64 @@ +===== +Setup +===== + +Setting up Blueprint on a new or existing project +------------------------------------------------- + +Using the porting tool +~~~~~~~~~~~~~~~~~~~~~~ + +Clone `blueprint-compiler `_ +from source. You can install it using ``meson _build`` and ``ninja -C _build install``, +or you can leave it uninstalled. + +In your project's directory, run ``blueprint-compiler port`` (or `` port``) +to start the porting process. It will walk you through the steps outlined below. +It should work for most projects, but if something goes wrong you may need to +follow the manual steps instead. + + +Manually +~~~~~~~~ + +blueprint-compiler works as a meson subproject. + +#. Save the following file as ``subprojects/blueprint-compiler.wrap``: + + .. code-block:: cfg + + [wrap-git] + directory = blueprint-compiler + url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git + revision = main + depth = 1 + + [provide] + program_names = blueprint-compiler + +#. Add this to your ``.gitignore``: + + .. code-block:: + + /subprojects/blueprint-compiler + +#. Rewrite your .ui XML files in blueprint format. + +#. Add this to the ``meson.build`` file where you build your GResources: + + .. code-block:: meson.build + + blueprints = custom_target('blueprints', + input: files( + # LIST YOUR BLUEPRINT FILES HERE + ), + output: '.', + command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], + ) + +#. In the same ``meson.build`` file, add this argument to your ``gnome.compile_resources`` command: + + .. code-block:: meson.build + + dependencies: blueprints, + diff --git a/gtk/blueprint-compiler/docs/translations.rst b/gtk/blueprint-compiler/docs/translations.rst new file mode 100644 index 00000000000..7ebf9296ec0 --- /dev/null +++ b/gtk/blueprint-compiler/docs/translations.rst @@ -0,0 +1,41 @@ +============ +Translations +============ + +Blueprint files can be translated with xgettext. To mark a string as translated, +use the following syntax: + +.. code-block:: blueprint + + _("translated string") + +You'll need to use a few xgettext flags so it will recognize the format: + +.. code-block:: + + --from-code=UTF-8 + --add-comments + --keyword=_ + --keyword=C_:1c,2 + +If you're using Meson's `i18n module `_, you can use the 'glib' preset: + +.. code-block:: meson.build + + i18n.gettext('package name', preset: 'glib') + +Contexts +-------- + +Unlike most other translation libraries, which use a separate string key, +gettext uses the English translation as the key. This is great for effortlessly +adding new strings--you just mark them as needing translation--but it can cause +conflicts. Two strings that are the same in English, but appear in different +contexts, might be different in another language! To disambiguate, use ``C_`` +instead of ``_`` and add a context string as the first argument: + +.. code-block:: blueprint + + C_("shortcuts window", "Quit") + +The context string will be shown to translators, but will not appear in the UI. \ No newline at end of file diff --git a/gtk/blueprint-compiler/justfile b/gtk/blueprint-compiler/justfile new file mode 100644 index 00000000000..37d85b58ed4 --- /dev/null +++ b/gtk/blueprint-compiler/justfile @@ -0,0 +1,22 @@ +default: black isort + +# Format with black formatter +black: + black ./ + +# Sort imports using isort +isort: + isort ./ --profile black + + +# Run all tests +test: mypy unittest + +# Check typings with mypy +mypy: + mypy --python-version=3.9 blueprintcompiler/ + +# Test code with unittest +unittest: + python3 -m unittest + diff --git a/gtk/blueprint-compiler/meson.build b/gtk/blueprint-compiler/meson.build new file mode 100644 index 00000000000..63d94893fa4 --- /dev/null +++ b/gtk/blueprint-compiler/meson.build @@ -0,0 +1,45 @@ +project('blueprint-compiler', + version: '0.14.0', +) + +prefix = get_option('prefix') +datadir = join_paths(prefix, get_option('datadir')) + +py = import('python').find_installation('python3') + +subdir('docs') + +configure_file( + input: 'blueprint-compiler.pc.in', + output: 'blueprint-compiler.pc', + configuration: { 'VERSION': meson.project_version() }, + install: not meson.is_subproject(), + install_dir: join_paths(datadir, 'pkgconfig'), +) + +config = configuration_data({ + 'VERSION': meson.project_version(), + 'LIBDIR': get_option('prefix') / get_option('libdir'), +}) + +if meson.is_subproject() + config.set('MODULE_PATH', meson.current_source_dir()) +else + config.set('MODULE_PATH', py.get_install_dir()) +endif + +blueprint_compiler = configure_file( + input: 'blueprint-compiler.py', + output: 'blueprint-compiler', + configuration: config, + install: not meson.is_subproject(), + install_dir: get_option('bindir'), +) + +if meson.is_subproject() + meson.override_find_program('blueprint-compiler', blueprint_compiler) +else + install_subdir('blueprintcompiler', install_dir: py.get_install_dir()) +endif + +subdir('tests') diff --git a/gtk/blueprint-compiler/meson_options.txt b/gtk/blueprint-compiler/meson_options.txt new file mode 100644 index 00000000000..c3c56611f10 --- /dev/null +++ b/gtk/blueprint-compiler/meson_options.txt @@ -0,0 +1 @@ +option('docs', type: 'boolean', value: false) diff --git a/gtk/blueprint-compiler/tests/__init__.py b/gtk/blueprint-compiler/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gtk/blueprint-compiler/tests/formatting/correct1.blp b/gtk/blueprint-compiler/tests/formatting/correct1.blp new file mode 100644 index 00000000000..aedc38a5fe5 --- /dev/null +++ b/gtk/blueprint-compiler/tests/formatting/correct1.blp @@ -0,0 +1,69 @@ +using Gtk 4.0; +using Adw 1; + +template $MyTemplate: Label { + /** + * A list of strings. + */ + StringList { + // comment + strings [ + "Hello", + C_("Greeting", "World"), + ] + } + + object: Button { + label: "Click me"; + }; + + flags: a | b; + + [child] + Label {} + + [child] + Label label2 {} + + // Single line comment. + + /** + * Multiline comment. + */ + // Single line comment. + value: bind (1.0) as ; + as: 1; + signal => $on_signal() after; // Inline comment + type_value: typeof<$MyTemplate>; +} + +Dialog { + [action response=ok] + $MyButton {} +} + +menu menu { + item ("test") + + item { + label: "test"; + } + + item ("test") +} + +Adw.MessageDialog { + responses [ + save: "Save" suggested disabled, + ] +} + +Adw.Breakpoint { + condition ("width < 100") + + setters { + label2.label: _("Hello, world!"); + label2.visible: false; + label2.extra-menu: null; + } +} diff --git a/gtk/blueprint-compiler/tests/formatting/in1.blp b/gtk/blueprint-compiler/tests/formatting/in1.blp new file mode 100644 index 00000000000..f77a3f4f88d --- /dev/null +++ b/gtk/blueprint-compiler/tests/formatting/in1.blp @@ -0,0 +1 @@ +using Gtk 4.0;using Adw 1;Overlay{Label label{label:_("'Hello World!' \"\n\t\"");}[overlay]Button{notify::icon-name=>$on_icon_name_changed(label)swapped;styles["destructive"]}visible:bind $isVisible(label.visible,my-menu)as;width-request:bind label.width-request no-sync-create;}menu my-menu{item(_("Label"), "action-name", "icon-name")item{action:"win.format";}} \ No newline at end of file diff --git a/gtk/blueprint-compiler/tests/formatting/in2.blp b/gtk/blueprint-compiler/tests/formatting/in2.blp new file mode 100644 index 00000000000..137871abafd --- /dev/null +++ b/gtk/blueprint-compiler/tests/formatting/in2.blp @@ -0,0 +1,40 @@ +using Gtk 4.0; + + using Adw 1; + +Overlay { + +Label +label +{ +label +: +_ +( +"'Hello World!' \"\n\t\"" +) +; +} +[ + overlay +] Button +{ notify +:: icon-name +=> $ on_icon_name_changed ( label ) +swapped ; +styles +[ "destructive" ] +} +visible +: bind $ isVisible ( label.visible , +my-menu ) as + < bool > ; width-request : bind label . width-request no-sync-create ; } + menu my-menu +{ item ( _ ( "Label" ) , "action-name" , "icon-name" ) item { action : "win.format" ; } } + + + + + + + diff --git a/gtk/blueprint-compiler/tests/formatting/out.blp b/gtk/blueprint-compiler/tests/formatting/out.blp new file mode 100644 index 00000000000..9d9a8b44fdf --- /dev/null +++ b/gtk/blueprint-compiler/tests/formatting/out.blp @@ -0,0 +1,28 @@ +using Gtk 4.0; +using Adw 1; + +Overlay { + Label label { + label: _("'Hello World!' \"\n\t\""); + } + + [overlay] + Button { + notify::icon-name => $on_icon_name_changed(label) swapped; + + styles [ + "destructive" + ] + } + + visible: bind $isVisible(label.visible, my-menu) as ; + width-request: bind label.width-request no-sync-create; +} + +menu my-menu { + item (_("Label"), "action-name", "icon-name") + + item { + action: "win.format"; + } +} diff --git a/gtk/blueprint-compiler/tests/formatting/string_in.blp b/gtk/blueprint-compiler/tests/formatting/string_in.blp new file mode 100644 index 00000000000..451d87909fa --- /dev/null +++ b/gtk/blueprint-compiler/tests/formatting/string_in.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: "\"'\'\t\n\\'"; +} diff --git a/gtk/blueprint-compiler/tests/formatting/string_out.blp b/gtk/blueprint-compiler/tests/formatting/string_out.blp new file mode 100644 index 00000000000..451d87909fa --- /dev/null +++ b/gtk/blueprint-compiler/tests/formatting/string_out.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: "\"'\'\t\n\\'"; +} diff --git a/gtk/blueprint-compiler/tests/fuzz.py b/gtk/blueprint-compiler/tests/fuzz.py new file mode 100644 index 00000000000..81a90586be1 --- /dev/null +++ b/gtk/blueprint-compiler/tests/fuzz.py @@ -0,0 +1,45 @@ +import os +import sys + +from pythonfuzz.main import PythonFuzz + +from blueprintcompiler.outputs.xml import XmlOutput + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + +from blueprintcompiler import decompiler, gir, parser, tokenizer, utils +from blueprintcompiler.completions import complete +from blueprintcompiler.errors import ( + CompileError, + CompilerBugError, + MultipleErrors, + PrintableError, +) +from blueprintcompiler.tokenizer import Token, TokenType, tokenize + + +@PythonFuzz +def fuzz(buf): + try: + blueprint = buf.decode("ascii") + + tokens = tokenizer.tokenize(blueprint) + ast, errors, warnings = parser.parse(tokens) + + xml = XmlOutput() + if errors is None and ast is not None: + xml.emit(ast) + except CompilerBugError as e: + raise e + except PrintableError: + pass + except UnicodeDecodeError: + pass + + +if __name__ == "__main__": + # Make sure Gtk 4.0 is accessible, otherwise every test will fail on that + # and nothing interesting will be tested + gir.get_namespace("Gtk", "4.0") + + fuzz() diff --git a/gtk/blueprint-compiler/tests/fuzz.sh b/gtk/blueprint-compiler/tests/fuzz.sh new file mode 100755 index 00000000000..eedccbcc038 --- /dev/null +++ b/gtk/blueprint-compiler/tests/fuzz.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +if [ $# = 1 ]; then + RUNS="$1" +else + RUNS=-1 +fi + +mkdir -p corpus +cp tests/samples/*.blp corpus +cp tests/sample_errors/*.blp corpus +python3 tests/fuzz.py --runs $RUNS corpus \ No newline at end of file diff --git a/gtk/blueprint-compiler/tests/meson.build b/gtk/blueprint-compiler/tests/meson.build new file mode 100644 index 00000000000..8a5a6343b05 --- /dev/null +++ b/gtk/blueprint-compiler/tests/meson.build @@ -0,0 +1 @@ +test('tests', py, args: ['-m', 'unittest'], workdir: meson.project_source_root()) diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_in_non_widget.blp b/gtk/blueprint-compiler/tests/sample_errors/a11y_in_non_widget.blp new file mode 100644 index 00000000000..9609a0c13d5 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_in_non_widget.blp @@ -0,0 +1,6 @@ +using Gtk 4.0; +using GObject 2.0; + +GObject.Object { + accessibility {} +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_in_non_widget.err b/gtk/blueprint-compiler/tests/sample_errors/a11y_in_non_widget.err new file mode 100644 index 00000000000..68d43dc2de5 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_in_non_widget.err @@ -0,0 +1 @@ +5,3,13,GObject.Object is not a Gtk.Widget, so it doesn't have accessibility properties diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_list_empty.blp b/gtk/blueprint-compiler/tests/sample_errors/a11y_list_empty.blp new file mode 100644 index 00000000000..401c912c1c5 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_list_empty.blp @@ -0,0 +1,9 @@ +using Gtk 4.0; + +Box { + accessibility { + label: _("Hello, world!"); + labelled-by: []; + checked: true; + } +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_list_empty.err b/gtk/blueprint-compiler/tests/sample_errors/a11y_list_empty.err new file mode 100644 index 00000000000..d2b0c868d1f --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_list_empty.err @@ -0,0 +1 @@ +6,5,11,'labelled-by' may not be empty diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_non_list_property.blp b/gtk/blueprint-compiler/tests/sample_errors/a11y_non_list_property.blp new file mode 100644 index 00000000000..daa3a96fc56 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_non_list_property.blp @@ -0,0 +1,15 @@ +using Gtk 4.0; + +Box { + accessibility { + label: _("Hello, world!"); + active-descendant: [my_label1, my_label2, my_label3]; + checked: true; + } +} + +Label my_label1 {} + +Label my_label2 {} + +Label my_label3 {} diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_non_list_property.err b/gtk/blueprint-compiler/tests/sample_errors/a11y_non_list_property.err new file mode 100644 index 00000000000..038da9245d5 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_non_list_property.err @@ -0,0 +1 @@ +6,5,17,'active-descendant' does not allow a list of values diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_dne.blp b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_dne.blp new file mode 100644 index 00000000000..ad444f03f39 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_dne.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +Button { + accessibility { + not_a_prop: "Hello, world!"; + } +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_dne.err b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_dne.err new file mode 100644 index 00000000000..b7a188d17db --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_dne.err @@ -0,0 +1 @@ +5,5,10,'not_a_prop' is not an accessibility property, relation, or state diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_obj_dne.blp b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_obj_dne.blp new file mode 100644 index 00000000000..79bba2ead3d --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_obj_dne.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +Button { + accessibility { + labelled-by: not_an_object; + } +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_obj_dne.err b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_obj_dne.err new file mode 100644 index 00000000000..ee66325b984 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_obj_dne.err @@ -0,0 +1 @@ +5,18,13,Could not find object with ID not_an_object diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_type.blp b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_type.blp new file mode 100644 index 00000000000..7fae7384e1b --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_type.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +Button { + accessibility { + orientation: 1; + } +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_type.err b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_type.err new file mode 100644 index 00000000000..cc0b0db634a --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/a11y_prop_type.err @@ -0,0 +1 @@ +5,18,1,Cannot convert number to Gtk.Orientation diff --git a/gtk/blueprint-compiler/tests/sample_errors/abstract_class.blp b/gtk/blueprint-compiler/tests/sample_errors/abstract_class.blp new file mode 100644 index 00000000000..dfb18191e3c --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/abstract_class.blp @@ -0,0 +1,4 @@ +using Gtk 4.0; + +template $MyWidget : Gtk.Widget {} +Gtk.Widget {} diff --git a/gtk/blueprint-compiler/tests/sample_errors/abstract_class.err b/gtk/blueprint-compiler/tests/sample_errors/abstract_class.err new file mode 100644 index 00000000000..1765fc4b211 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/abstract_class.err @@ -0,0 +1 @@ +4,1,10,Gtk.Widget can't be instantiated because it's abstract diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_float_response.blp b/gtk/blueprint-compiler/tests/sample_errors/action_widget_float_response.blp new file mode 100644 index 00000000000..d175304bc62 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_float_response.blp @@ -0,0 +1,8 @@ +using Gtk 4.0; + +Dialog { + [action response=17.9] + Button float_response_button { + + } +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_float_response.err b/gtk/blueprint-compiler/tests/sample_errors/action_widget_float_response.err new file mode 100644 index 00000000000..b2611fa7a39 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_float_response.err @@ -0,0 +1 @@ +4,22,4,Response type must be GtkResponseType member or integer, not float diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_have_no_id.blp b/gtk/blueprint-compiler/tests/sample_errors/action_widget_have_no_id.blp new file mode 100644 index 00000000000..b216a28ad37 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_have_no_id.blp @@ -0,0 +1,8 @@ +using Gtk 4.0; + +Dialog { + [action response=cancel] + Button { + label: _("Cancel"); + } +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_have_no_id.err b/gtk/blueprint-compiler/tests/sample_errors/action_widget_have_no_id.err new file mode 100644 index 00000000000..b239d7786d0 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_have_no_id.err @@ -0,0 +1 @@ +4,6,22,Action widget must have ID diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_in_invalid_container.blp b/gtk/blueprint-compiler/tests/sample_errors/action_widget_in_invalid_container.blp new file mode 100644 index 00000000000..7c17cb200a0 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_in_invalid_container.blp @@ -0,0 +1,8 @@ +using Gtk 4.0; + +Box { + [action response=ok] + Button ok_button { + + } +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_in_invalid_container.err b/gtk/blueprint-compiler/tests/sample_errors/action_widget_in_invalid_container.err new file mode 100644 index 00000000000..ef3296c3026 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_in_invalid_container.err @@ -0,0 +1 @@ +4,6,18,Gtk.Box doesn't have action widgets diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_multiple_default.blp b/gtk/blueprint-compiler/tests/sample_errors/action_widget_multiple_default.blp new file mode 100644 index 00000000000..bd570d31774 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_multiple_default.blp @@ -0,0 +1,13 @@ +using Gtk 4.0; + +Dialog { + [action response=yes default] + Button yes_button { + + } + + [action response=no default] + Button no_button { + + } +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_multiple_default.err b/gtk/blueprint-compiler/tests/sample_errors/action_widget_multiple_default.err new file mode 100644 index 00000000000..3c35743ce30 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_multiple_default.err @@ -0,0 +1 @@ +9,25,7,Default response is already set diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_negative_response.blp b/gtk/blueprint-compiler/tests/sample_errors/action_widget_negative_response.blp new file mode 100644 index 00000000000..6fde756df98 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_negative_response.blp @@ -0,0 +1,8 @@ +using Gtk 4.0; + +Dialog { + [action response = -179] + Button numeric_response_button { + + } +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_negative_response.err b/gtk/blueprint-compiler/tests/sample_errors/action_widget_negative_response.err new file mode 100644 index 00000000000..6f0a627cfa2 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_negative_response.err @@ -0,0 +1 @@ +4,25,3,Numeric response type can't be negative diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_response_dne.blp b/gtk/blueprint-compiler/tests/sample_errors/action_widget_response_dne.blp new file mode 100644 index 00000000000..e260d3a6bce --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_response_dne.blp @@ -0,0 +1,8 @@ +using Gtk 4.0; + +Dialog { + [action response=hello-world] + Button hello_world_button { + + } +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/action_widget_response_dne.err b/gtk/blueprint-compiler/tests/sample_errors/action_widget_response_dne.err new file mode 100644 index 00000000000..67685f40798 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/action_widget_response_dne.err @@ -0,0 +1 @@ +4,22,11,Response type "hello-world" doesn't exist diff --git a/gtk/blueprint-compiler/tests/sample_errors/adw_alert_dialog_duplicate_flags.blp b/gtk/blueprint-compiler/tests/sample_errors/adw_alert_dialog_duplicate_flags.blp new file mode 100644 index 00000000000..65fae2276dd --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/adw_alert_dialog_duplicate_flags.blp @@ -0,0 +1,9 @@ +using Gtk 4.0; +using Adw 1; + +Adw.AlertDialog { + responses [ + cancel: _("Cancel") disabled disabled, + ok: _("Ok") destructive suggested, + ] +} \ No newline at end of file diff --git a/gtk/blueprint-compiler/tests/sample_errors/adw_alert_dialog_duplicate_flags.err b/gtk/blueprint-compiler/tests/sample_errors/adw_alert_dialog_duplicate_flags.err new file mode 100644 index 00000000000..91f0a3d5f5d --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/adw_alert_dialog_duplicate_flags.err @@ -0,0 +1,2 @@ +6,34,8,Duplicate 'disabled' flag +7,29,9,'suggested' and 'destructive' are exclusive \ No newline at end of file diff --git a/gtk/blueprint-compiler/tests/sample_errors/adw_breakpoint.blp b/gtk/blueprint-compiler/tests/sample_errors/adw_breakpoint.blp new file mode 100644 index 00000000000..7eb45258cfa --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/adw_breakpoint.blp @@ -0,0 +1,12 @@ +using Gtk 4.0; +using Adw 1; + +Label label {} + +Adw.Breakpoint { + setters { + label.foo: "bar"; + not_an_object.visible: true; + label.foo: "baz"; + } +} \ No newline at end of file diff --git a/gtk/blueprint-compiler/tests/sample_errors/adw_breakpoint.err b/gtk/blueprint-compiler/tests/sample_errors/adw_breakpoint.err new file mode 100644 index 00000000000..bd2f6b75e23 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/adw_breakpoint.err @@ -0,0 +1,4 @@ +8,11,3,Class Gtk.Label does not have a property called foo +9,5,13,Could not find object with ID not_an_object +10,11,3,Class Gtk.Label does not have a property called foo +10,5,17,Duplicate setter for label.foo \ No newline at end of file diff --git a/gtk/blueprint-compiler/tests/sample_errors/adw_message_dialog_duplicate_flags.blp b/gtk/blueprint-compiler/tests/sample_errors/adw_message_dialog_duplicate_flags.blp new file mode 100644 index 00000000000..466db6e08aa --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/adw_message_dialog_duplicate_flags.blp @@ -0,0 +1,9 @@ +using Gtk 4.0; +using Adw 1; + +Adw.MessageDialog { + responses [ + cancel: _("Cancel") disabled disabled, + ok: _("Ok") destructive suggested, + ] +} \ No newline at end of file diff --git a/gtk/blueprint-compiler/tests/sample_errors/adw_message_dialog_duplicate_flags.err b/gtk/blueprint-compiler/tests/sample_errors/adw_message_dialog_duplicate_flags.err new file mode 100644 index 00000000000..91f0a3d5f5d --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/adw_message_dialog_duplicate_flags.err @@ -0,0 +1,2 @@ +6,34,8,Duplicate 'disabled' flag +7,29,9,'suggested' and 'destructive' are exclusive \ No newline at end of file diff --git a/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type.blp b/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type.blp new file mode 100644 index 00000000000..c8cdd9bf255 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: [1]; +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type.err b/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type.err new file mode 100644 index 00000000000..20e8e0d6f7b --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type.err @@ -0,0 +1 @@ +4,12,3,Cannot assign array to string \ No newline at end of file diff --git a/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type_value.blp b/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type_value.blp new file mode 100644 index 00000000000..9b39e002af9 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type_value.blp @@ -0,0 +1,6 @@ +using Gtk 4.0; + +AboutDialog about { + valign: center; + authors: [1]; +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type_value.err b/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type_value.err new file mode 100644 index 00000000000..7ba59a841d1 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/array_wrong_type_value.err @@ -0,0 +1 @@ +5,15,1,Cannot convert number to string \ No newline at end of file diff --git a/gtk/blueprint-compiler/tests/sample_errors/bad_escape_sequence.blp b/gtk/blueprint-compiler/tests/sample_errors/bad_escape_sequence.blp new file mode 100644 index 00000000000..4b109c6639f --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/bad_escape_sequence.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: '***** \f *****'; +} diff --git a/gtk/blueprint-compiler/tests/sample_errors/bad_escape_sequence.err b/gtk/blueprint-compiler/tests/sample_errors/bad_escape_sequence.err new file mode 100644 index 00000000000..e4ec1838ad2 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/bad_escape_sequence.err @@ -0,0 +1 @@ +4,17,2,Invalid escape sequence '\f' \ No newline at end of file diff --git a/gtk/blueprint-compiler/tests/sample_errors/binding_flags.blp b/gtk/blueprint-compiler/tests/sample_errors/binding_flags.blp new file mode 100644 index 00000000000..7e940583517 --- /dev/null +++ b/gtk/blueprint-compiler/tests/sample_errors/binding_flags.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: bind $my_closure() as