diff --git a/sixtyfps_compiler/lib.rs b/sixtyfps_compiler/lib.rs index 6e8e2aba4..a238d42e3 100644 --- a/sixtyfps_compiler/lib.rs +++ b/sixtyfps_compiler/lib.rs @@ -34,6 +34,7 @@ pub mod generator; pub mod langtype; pub mod layout; pub mod lexer; +pub mod literals; pub(crate) mod load_builtins; pub mod lookup; pub mod namedreference; diff --git a/sixtyfps_compiler/literals.rs b/sixtyfps_compiler/literals.rs new file mode 100644 index 000000000..f2819449e --- /dev/null +++ b/sixtyfps_compiler/literals.rs @@ -0,0 +1,168 @@ +/* LICENSE BEGIN + This file is part of the SixtyFPS Project -- https://sixtyfps.io + Copyright (c) 2020 Olivier Goffart + Copyright (c) 2020 Simon Hausmann + + SPDX-License-Identifier: GPL-3.0-only + This file is also available under commercial licensing terms. + Please contact info@sixtyfps.io for more information. +LICENSE END */ + +use crate::expression_tree::Expression; + +/// Returns `0xaarrggbb` +pub fn parse_color_literal(str: &str) -> Option { + if !str.starts_with('#') { + return None; + } + if !str.is_ascii() { + return None; + } + let str = &str[1..]; + let (r, g, b, a) = match str.len() { + 3 => ( + u8::from_str_radix(&str[0..=0], 16).ok()? * 0x11, + u8::from_str_radix(&str[1..=1], 16).ok()? * 0x11, + u8::from_str_radix(&str[2..=2], 16).ok()? * 0x11, + 255u8, + ), + 4 => ( + u8::from_str_radix(&str[0..=0], 16).ok()? * 0x11, + u8::from_str_radix(&str[1..=1], 16).ok()? * 0x11, + u8::from_str_radix(&str[2..=2], 16).ok()? * 0x11, + u8::from_str_radix(&str[3..=3], 16).ok()? * 0x11, + ), + 6 => ( + u8::from_str_radix(&str[0..2], 16).ok()?, + u8::from_str_radix(&str[2..4], 16).ok()?, + u8::from_str_radix(&str[4..6], 16).ok()?, + 255u8, + ), + 8 => ( + u8::from_str_radix(&str[0..2], 16).ok()?, + u8::from_str_radix(&str[2..4], 16).ok()?, + u8::from_str_radix(&str[4..6], 16).ok()?, + u8::from_str_radix(&str[6..8], 16).ok()?, + ), + _ => return None, + }; + Some((a as u32) << 24 | (r as u32) << 16 | (g as u32) << 8 | (b as u32)) +} + +#[test] +fn test_parse_color_literal() { + assert_eq!(parse_color_literal("#abc"), Some(0xffaabbcc)); + assert_eq!(parse_color_literal("#ABC"), Some(0xffaabbcc)); + assert_eq!(parse_color_literal("#AbC"), Some(0xffaabbcc)); + assert_eq!(parse_color_literal("#AbCd"), Some(0xddaabbcc)); + assert_eq!(parse_color_literal("#01234567"), Some(0x67012345)); + assert_eq!(parse_color_literal("#012345"), Some(0xff012345)); + assert_eq!(parse_color_literal("_01234567"), None); + assert_eq!(parse_color_literal("→↓←"), None); + assert_eq!(parse_color_literal("#→↓←"), None); + assert_eq!(parse_color_literal("#1234567890"), None); +} + +pub fn unescape_string(string: &str) -> Option { + if string.contains('\n') { + // FIXME: new line in string literal not yet supported + return None; + } + let string = string.strip_prefix('"').or_else(|| string.strip_prefix('}'))?; + let string = string.strip_suffix('"').or_else(|| string.strip_suffix("\\{"))?; + if !string.contains('\\') { + return Some(string.into()); + } + let mut result = String::with_capacity(string.len()); + let mut pos = 0; + loop { + let stop = match string[pos..].find('\\') { + Some(stop) => pos + stop, + None => { + result += &string[pos..]; + return Some(result); + } + }; + if stop + 1 >= string.len() { + return None; + } + result += &string[pos..stop]; + pos = stop + 2; + match string.as_bytes()[stop + 1] { + b'"' => result += "\"", + b'\\' => result += "\\", + b'n' => result += "\n", + b'u' => { + if string.as_bytes().get(pos)? != &b'{' { + return None; + } + let end = string[pos..].find('}')? + pos; + let x = u32::from_str_radix(&string[pos + 1..end], 16).ok()?; + result.push(std::char::from_u32(x)?); + pos = end + 1; + } + _ => return None, + } + } +} + +#[test] +fn test_unsecape_string() { + assert_eq!(unescape_string(r#""foo_bar""#), Some("foo_bar".into())); + assert_eq!(unescape_string(r#""foo\"bar""#), Some("foo\"bar".into())); + assert_eq!(unescape_string(r#""foo\\\"bar""#), Some("foo\\\"bar".into())); + assert_eq!(unescape_string(r#""fo\na\\r""#), Some("fo\na\\r".into())); + assert_eq!(unescape_string(r#""fo\xa""#), None); + assert_eq!(unescape_string(r#""fooo\""#), None); + assert_eq!(unescape_string(r#""f\n\n\nf""#), Some("f\n\n\nf".into())); + assert_eq!(unescape_string(r#""music\♪xx""#), None); + assert_eq!(unescape_string(r#""music\"♪\"🎝""#), Some("music\"♪\"🎝".into())); + assert_eq!(unescape_string(r#""foo_bar"#), None); + assert_eq!(unescape_string(r#""foo_bar\"#), None); + assert_eq!(unescape_string(r#"foo_bar""#), None); + assert_eq!(unescape_string(r#""d\u{8}a\u{d4}f\u{Ed3}""#), Some("d\u{8}a\u{d4}f\u{ED3}".into())); + assert_eq!(unescape_string(r#""xxx\""#), None); + assert_eq!(unescape_string(r#""xxx\u""#), None); + assert_eq!(unescape_string(r#""xxx\uxx""#), None); + assert_eq!(unescape_string(r#""xxx\u{""#), None); + assert_eq!(unescape_string(r#""xxx\u{22""#), None); + assert_eq!(unescape_string(r#""xxx\u{qsdf}""#), None); + assert_eq!(unescape_string(r#""xxx\u{1234567890}""#), None); +} + +pub fn parse_number_literal(s: String) -> Result { + let bytes = s.as_bytes(); + let mut end = 0; + while end < bytes.len() && matches!(bytes[end], b'0'..=b'9' | b'.') { + end += 1; + } + let val = s[..end].parse().map_err(|_| "Cannot parse number literal".to_owned())?; + let unit = s[end..].parse().map_err(|_| "Invalid unit".to_owned())?; + Ok(Expression::NumberLiteral(val, unit)) +} + +#[test] +fn test_parse_number_literal() { + fn doit(s: &str) -> Result<(f64, Unit), String> { + parse_number_literal(s.into()).map(|e| match e { + Expression::NumberLiteral(a, b) => (a, b), + _ => panic!(), + }) + } + + assert_eq!(doit("10"), Ok((10., Unit::None))); + assert_eq!(doit("10phx"), Ok((10., Unit::Phx))); + assert_eq!(doit("10.0phx"), Ok((10., Unit::Phx))); + assert_eq!(doit("10.0"), Ok((10., Unit::None))); + assert_eq!(doit("1.1phx"), Ok((1.1, Unit::Phx))); + assert_eq!(doit("10.10"), Ok((10.10, Unit::None))); + assert_eq!(doit("10000000"), Ok((10000000., Unit::None))); + assert_eq!(doit("10000001phx"), Ok((10000001., Unit::Phx))); + + let wrong_unit = Err("Invalid unit".to_owned()); + let cannot_parse = Err("Cannot parse number literal".to_owned()); + assert_eq!(doit("10000001 phx"), wrong_unit); + assert_eq!(doit("12.10.12phx"), cannot_parse); + assert_eq!(doit("12.12oo"), wrong_unit); + assert_eq!(doit("12.12€"), wrong_unit); +} diff --git a/sixtyfps_compiler/passes/resolving.rs b/sixtyfps_compiler/passes/resolving.rs index 07ddc4053..54e825531 100644 --- a/sixtyfps_compiler/passes/resolving.rs +++ b/sixtyfps_compiler/passes/resolving.rs @@ -250,15 +250,17 @@ impl Expression { .or_else(|| node.QualifiedName().map(|s| Self::from_qualified_name_node(s.into(), ctx))) .or_else(|| { node.child_text(SyntaxKind::StringLiteral).map(|s| { - unescape_string(&s).map(Self::StringLiteral).unwrap_or_else(|| { - ctx.diag.push_error("Cannot parse string literal".into(), &node); - Self::Invalid - }) + crate::literals::unescape_string(&s).map(Self::StringLiteral).unwrap_or_else( + || { + ctx.diag.push_error("Cannot parse string literal".into(), &node); + Self::Invalid + }, + ) }) }) .or_else(|| { node.child_text(SyntaxKind::NumberLiteral) - .map(parse_number_literal) + .map(crate::literals::parse_number_literal) .transpose() .unwrap_or_else(|e| { ctx.diag.push_error(e, &node); @@ -267,7 +269,7 @@ impl Expression { }) .or_else(|| { node.child_text(SyntaxKind::ColorLiteral).map(|s| { - parse_color_literal(&s) + crate::literals::parse_color_literal(&s) .map(|i| Expression::Cast { from: Box::new(Expression::NumberLiteral(i as _, Unit::None)), to: Type::Color, @@ -297,7 +299,10 @@ impl Expression { } fn from_at_image_url_node(node: syntax_nodes::AtImageUrl, ctx: &mut LookupCtx) -> Self { - let s = match node.child_text(SyntaxKind::StringLiteral).and_then(|x| unescape_string(&x)) { + let s = match node + .child_text(SyntaxKind::StringLiteral) + .and_then(|x| crate::literals::unescape_string(&x)) + { Some(s) => s, None => { ctx.diag.push_error("Cannot parse string literal".into(), &node); @@ -1140,159 +1145,3 @@ fn maybe_lookup_object( } base } - -fn parse_color_literal(str: &str) -> Option { - if !str.starts_with('#') { - return None; - } - if !str.is_ascii() { - return None; - } - let str = &str[1..]; - let (r, g, b, a) = match str.len() { - 3 => ( - u8::from_str_radix(&str[0..=0], 16).ok()? * 0x11, - u8::from_str_radix(&str[1..=1], 16).ok()? * 0x11, - u8::from_str_radix(&str[2..=2], 16).ok()? * 0x11, - 255u8, - ), - 4 => ( - u8::from_str_radix(&str[0..=0], 16).ok()? * 0x11, - u8::from_str_radix(&str[1..=1], 16).ok()? * 0x11, - u8::from_str_radix(&str[2..=2], 16).ok()? * 0x11, - u8::from_str_radix(&str[3..=3], 16).ok()? * 0x11, - ), - 6 => ( - u8::from_str_radix(&str[0..2], 16).ok()?, - u8::from_str_radix(&str[2..4], 16).ok()?, - u8::from_str_radix(&str[4..6], 16).ok()?, - 255u8, - ), - 8 => ( - u8::from_str_radix(&str[0..2], 16).ok()?, - u8::from_str_radix(&str[2..4], 16).ok()?, - u8::from_str_radix(&str[4..6], 16).ok()?, - u8::from_str_radix(&str[6..8], 16).ok()?, - ), - _ => return None, - }; - Some((a as u32) << 24 | (r as u32) << 16 | (g as u32) << 8 | (b as u32)) -} - -#[test] -fn test_parse_color_literal() { - assert_eq!(parse_color_literal("#abc"), Some(0xffaabbcc)); - assert_eq!(parse_color_literal("#ABC"), Some(0xffaabbcc)); - assert_eq!(parse_color_literal("#AbC"), Some(0xffaabbcc)); - assert_eq!(parse_color_literal("#AbCd"), Some(0xddaabbcc)); - assert_eq!(parse_color_literal("#01234567"), Some(0x67012345)); - assert_eq!(parse_color_literal("#012345"), Some(0xff012345)); - assert_eq!(parse_color_literal("_01234567"), None); - assert_eq!(parse_color_literal("→↓←"), None); - assert_eq!(parse_color_literal("#→↓←"), None); - assert_eq!(parse_color_literal("#1234567890"), None); -} - -fn unescape_string(string: &str) -> Option { - if string.contains('\n') { - // FIXME: new line in string literal not yet supported - return None; - } - let string = string.strip_prefix('"').or_else(|| string.strip_prefix('}'))?; - let string = string.strip_suffix('"').or_else(|| string.strip_suffix("\\{"))?; - if !string.contains('\\') { - return Some(string.into()); - } - let mut result = String::with_capacity(string.len()); - let mut pos = 0; - loop { - let stop = match string[pos..].find('\\') { - Some(stop) => pos + stop, - None => { - result += &string[pos..]; - return Some(result); - } - }; - if stop + 1 >= string.len() { - return None; - } - result += &string[pos..stop]; - pos = stop + 2; - match string.as_bytes()[stop + 1] { - b'"' => result += "\"", - b'\\' => result += "\\", - b'n' => result += "\n", - b'u' => { - if string.as_bytes().get(pos)? != &b'{' { - return None; - } - let end = string[pos..].find('}')? + pos; - let x = u32::from_str_radix(&string[pos + 1..end], 16).ok()?; - result.push(std::char::from_u32(x)?); - pos = end + 1; - } - _ => return None, - } - } -} - -#[test] -fn test_unsecape_string() { - assert_eq!(unescape_string(r#""foo_bar""#), Some("foo_bar".into())); - assert_eq!(unescape_string(r#""foo\"bar""#), Some("foo\"bar".into())); - assert_eq!(unescape_string(r#""foo\\\"bar""#), Some("foo\\\"bar".into())); - assert_eq!(unescape_string(r#""fo\na\\r""#), Some("fo\na\\r".into())); - assert_eq!(unescape_string(r#""fo\xa""#), None); - assert_eq!(unescape_string(r#""fooo\""#), None); - assert_eq!(unescape_string(r#""f\n\n\nf""#), Some("f\n\n\nf".into())); - assert_eq!(unescape_string(r#""music\♪xx""#), None); - assert_eq!(unescape_string(r#""music\"♪\"🎝""#), Some("music\"♪\"🎝".into())); - assert_eq!(unescape_string(r#""foo_bar"#), None); - assert_eq!(unescape_string(r#""foo_bar\"#), None); - assert_eq!(unescape_string(r#"foo_bar""#), None); - assert_eq!(unescape_string(r#""d\u{8}a\u{d4}f\u{Ed3}""#), Some("d\u{8}a\u{d4}f\u{ED3}".into())); - assert_eq!(unescape_string(r#""xxx\""#), None); - assert_eq!(unescape_string(r#""xxx\u""#), None); - assert_eq!(unescape_string(r#""xxx\uxx""#), None); - assert_eq!(unescape_string(r#""xxx\u{""#), None); - assert_eq!(unescape_string(r#""xxx\u{22""#), None); - assert_eq!(unescape_string(r#""xxx\u{qsdf}""#), None); - assert_eq!(unescape_string(r#""xxx\u{1234567890}""#), None); -} - -fn parse_number_literal(s: String) -> Result { - let bytes = s.as_bytes(); - let mut end = 0; - while end < bytes.len() && matches!(bytes[end], b'0'..=b'9' | b'.') { - end += 1; - } - let val = s[..end].parse().map_err(|_| "Cannot parse number literal".to_owned())?; - let unit = s[end..].parse().map_err(|_| "Invalid unit".to_owned())?; - Ok(Expression::NumberLiteral(val, unit)) -} - -#[test] -fn test_parse_number_literal() { - fn doit(s: &str) -> Result<(f64, Unit), String> { - parse_number_literal(s.into()).map(|e| match e { - Expression::NumberLiteral(a, b) => (a, b), - _ => panic!(), - }) - } - - assert_eq!(doit("10"), Ok((10., Unit::None))); - assert_eq!(doit("10phx"), Ok((10., Unit::Phx))); - assert_eq!(doit("10.0phx"), Ok((10., Unit::Phx))); - assert_eq!(doit("10.0"), Ok((10., Unit::None))); - assert_eq!(doit("1.1phx"), Ok((1.1, Unit::Phx))); - assert_eq!(doit("10.10"), Ok((10.10, Unit::None))); - assert_eq!(doit("10000000"), Ok((10000000., Unit::None))); - assert_eq!(doit("10000001phx"), Ok((10000001., Unit::Phx))); - - let wrong_unit = Err("Invalid unit".to_owned()); - let cannot_parse = Err("Cannot parse number literal".to_owned()); - assert_eq!(doit("10000001 phx"), wrong_unit); - assert_eq!(doit("12.10.12phx"), cannot_parse); - assert_eq!(doit("12.12oo"), wrong_unit); - assert_eq!(doit("12.12€"), wrong_unit); -} diff --git a/tools/lsp/goto.rs b/tools/lsp/goto.rs index 18dc45228..8c0b04db5 100644 --- a/tools/lsp/goto.rs +++ b/tools/lsp/goto.rs @@ -11,7 +11,7 @@ LICENSE END */ use std::path::Path; use super::DocumentCache; -use lsp_types::{GotoDefinitionResponse, LocationLink, Position, Range, Url}; +use lsp_types::{GotoDefinitionResponse, LocationLink, Range, Url}; use sixtyfps_compilerlib::diagnostics::Spanned; use sixtyfps_compilerlib::expression_tree::Expression; use sixtyfps_compilerlib::langtype::Type; @@ -191,26 +191,8 @@ fn goto_node( ) -> Option { let path = node.source_file.path(); let target_uri = Url::from_file_path(path).ok()?; - let newline_offsets = match document_cache.newline_offsets.entry(target_uri.clone()) { - std::collections::hash_map::Entry::Occupied(e) => e.into_mut(), - std::collections::hash_map::Entry::Vacant(e) => e.insert( - DocumentCache::newline_offsets_from_content(&std::fs::read_to_string(path).ok()?), - ), - }; let offset = node.span().offset as u32; - let pos = newline_offsets.binary_search(&offset).map_or_else( - |line| { - if line == 0 { - Position::new(0, offset) - } else { - Position::new( - line as u32 - 1, - newline_offsets.get(line - 1).map_or(0, |x| offset - *x), - ) - } - }, - |line| Position::new(line as u32, 0), - ); + let pos = document_cache.byte_offset_to_position(offset, &target_uri)?; let range = Range::new(pos, pos); Some(GotoDefinitionResponse::Link(vec![LocationLink { origin_selection_range: None, diff --git a/tools/lsp/main.rs b/tools/lsp/main.rs index f97664a66..a91cd16f7 100644 --- a/tools/lsp/main.rs +++ b/tools/lsp/main.rs @@ -18,13 +18,14 @@ use std::collections::HashMap; use lsp_server::{Connection, Message, Request, RequestId, Response}; use lsp_types::notification::{DidChangeTextDocument, DidOpenTextDocument, Notification}; -use lsp_types::request::{CodeActionRequest, ExecuteCommand, GotoDefinition}; +use lsp_types::request::{CodeActionRequest, DocumentColor, ExecuteCommand, GotoDefinition}; use lsp_types::request::{Completion, HoverRequest}; use lsp_types::{ - CodeActionOrCommand, CodeActionProviderCapability, Command, CompletionOptions, - DidChangeTextDocumentParams, DidOpenTextDocumentParams, ExecuteCommandOptions, Hover, - HoverProviderCapability, InitializeParams, OneOf, Position, PublishDiagnosticsParams, Range, - ServerCapabilities, TextDocumentSyncCapability, Url, WorkDoneProgressOptions, + CodeActionOrCommand, CodeActionProviderCapability, Color, ColorInformation, Command, + CompletionOptions, DidChangeTextDocumentParams, DidOpenTextDocumentParams, + ExecuteCommandOptions, Hover, HoverProviderCapability, InitializeParams, OneOf, Position, + PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability, Url, + WorkDoneProgressOptions, }; use sixtyfps_compilerlib::diagnostics::BuildDiagnostics; use sixtyfps_compilerlib::langtype::Type; @@ -60,6 +61,35 @@ impl<'a> DocumentCache<'a> { }) .collect() } + + pub fn byte_offset_to_position( + &mut self, + offset: u32, + target_uri: &lsp_types::Url, + ) -> Option { + let newline_offsets = match self.newline_offsets.entry(target_uri.clone()) { + std::collections::hash_map::Entry::Occupied(e) => e.into_mut(), + std::collections::hash_map::Entry::Vacant(e) => { + e.insert(Self::newline_offsets_from_content( + &std::fs::read_to_string(target_uri.to_file_path().ok()?).ok()?, + )) + } + }; + let pos = newline_offsets.binary_search(&offset).map_or_else( + |line| { + if line == 0 { + Position::new(0, offset) + } else { + Position::new( + line as u32 - 1, + newline_offsets.get(line - 1).map_or(0, |x| offset - *x), + ) + } + }, + |line| Position::new(line as u32, 0), + ); + Some(pos) + } } fn main() { @@ -97,6 +127,7 @@ fn run_lsp_server() -> Result<(), Error> { commands: vec![SHOW_PREVIEW_COMMAND.into()], ..Default::default() }), + color_provider: Some(true.into()), ..ServerCapabilities::default() }; let server_capabilities = serde_json::to_value(&capabilities).unwrap(); @@ -189,6 +220,9 @@ fn handle_request( connection .sender .send(Message::Response(Response::new_ok(id, None::)))?; + } else if let Some((id, params)) = cast::(&mut req) { + let result = get_document_color(document_cache, ¶ms.text_document).unwrap_or_default(); + connection.sender.send(Message::Response(Response::new_ok(id, result)))?; }; Ok(()) } @@ -403,3 +437,40 @@ fn get_code_actions( Some(vec![component.source_file.path().to_string_lossy().into(), component_name.into()]), ))]) } + +fn get_document_color( + document_cache: &mut DocumentCache, + text_document: &lsp_types::TextDocumentIdentifier, +) -> Option> { + let mut result = Vec::new(); + let uri = &text_document.uri; + let doc = document_cache.documents.get_document(&uri.to_file_path().ok()?)?; + let root_node = &doc.node.as_ref()?.node; + let mut token = root_node.first_token()?; + loop { + if token.kind() == SyntaxKind::ColorLiteral { + (|| -> Option<()> { + let range = token.text_range(); + let col = sixtyfps_compilerlib::literals::parse_color_literal(token.text())?; + let shift = |s: u32| -> f32 { ((col >> s) & 0xff) as f32 / 255. }; + result.push(ColorInformation { + range: Range::new( + document_cache.byte_offset_to_position(range.start().into(), &uri)?, + document_cache.byte_offset_to_position(range.end().into(), &uri)?, + ), + color: Color { + alpha: shift(24), + red: shift(16), + green: shift(8), + blue: shift(0), + }, + }); + Some(()) + })(); + } + token = match token.next_token() { + Some(token) => token, + None => break Some(result), + } + } +}