Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-core / src / line_numbers.rs
7.5 kB 274 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2021 The Gleam contributors 3 4use crate::ast::SrcSpan; 5use lsp_types::Position; 6use std::collections::HashMap; 7 8/// A struct which contains information about line numbers of a source file, 9/// and can convert between byte offsets that are used in the compiler and 10/// line-column pairs used in LSP. 11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] 12pub struct LineNumbers { 13 /// The byte offsets of the start of each line of the source file 14 pub line_starts: Vec<u32>, 15 /// The total length of the source file 16 pub length: u32, 17 /// A mapping of byte offsets to character length information. This is used 18 /// when converting between byte indices and line-column numbers, because 19 /// LSP uses UTF-16, while Rust encodes strings as UTF-8. 20 /// 21 /// This only contains characters which are more than one byte in UTF-8, 22 /// because one byte UTF-8 characters are one UTF-16 segment also, so no 23 /// translation is needed. 24 /// 25 /// We could store the whole source file here instead, however that would 26 /// be quite wasteful. Most Gleam programs use only ASCII characters, meaning 27 /// UTF-8 offsets are the same as UTF-16 ones. With this representation, we 28 /// only need to store a few characters. 29 /// 30 /// In most programs this will be empty because they will only be using 31 /// ASCII characters. 32 pub mapping: HashMap<usize, Character>, 33} 34 35/// Information about how a character is encoded in UTF-8 and UTF-16. 36#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)] 37pub struct Character { 38 /// The number of bytes needed to encode this in UTF-8. 39 pub length_utf8: u8, 40 /// The number of 16-bit segments needed to encode this in UTF-16. 41 pub length_utf16: u8, 42} 43 44impl LineNumbers { 45 pub fn new(src: &str) -> Self { 46 Self { 47 length: src.len() as u32, 48 line_starts: std::iter::once(0) 49 .chain(src.match_indices('\n').map(|(i, _)| i as u32 + 1)) 50 .collect(), 51 mapping: Self::mapping(src), 52 } 53 } 54 55 fn mapping(src: &str) -> HashMap<usize, Character> { 56 let mut map = HashMap::new(); 57 58 for (i, char) in src.char_indices() { 59 let length = char.len_utf8(); 60 if length != 1 { 61 _ = map.insert( 62 i, 63 Character { 64 length_utf8: length as u8, 65 length_utf16: char.len_utf16() as u8, 66 }, 67 ); 68 } 69 } 70 71 map 72 } 73 74 /// Returns the 1-indexed line number of a given byte index 75 pub fn line_number(&self, byte_index: u32) -> u32 { 76 self.line_starts 77 .binary_search(&byte_index) 78 .unwrap_or_else(|next_line| next_line - 1) as u32 79 + 1 80 } 81 82 /// Returns the 1-indexed line and column number of a given byte index, 83 /// using a UTF-16 character offset. 84 pub fn line_and_column_number(&self, byte_index: u32) -> LineColumn { 85 let line = self.line_number(byte_index); 86 let line_start = self 87 .line_starts 88 .get(line as usize - 1) 89 .copied() 90 .unwrap_or_default(); 91 92 let mut u8_offset = line_start; 93 let mut u16_offset = 0; 94 95 loop { 96 if u8_offset >= byte_index { 97 break; 98 } 99 100 if let Some(length) = self.mapping.get(&(u8_offset as usize)) { 101 u8_offset += length.length_utf8 as u32; 102 u16_offset += length.length_utf16 as u32; 103 } else { 104 u16_offset += 1; 105 u8_offset += 1; 106 } 107 } 108 109 LineColumn { 110 line, 111 column: u16_offset + 1, 112 } 113 } 114 115 /// Returns the byte index of the corresponding LSP line-column `Position`, 116 /// translating from a UTF-16 character index to a UTF-8 byte index. 117 pub fn byte_index(&self, position: Position) -> u32 { 118 let line_start = match self.line_starts.get(position.line as usize) { 119 Some(&line_start) => line_start, 120 None => return self.length, 121 }; 122 123 let mut u8_offset = line_start; 124 let mut u16_offset = 0; 125 126 loop { 127 if u16_offset >= position.character { 128 break; 129 } 130 131 if let Some(length) = self.mapping.get(&(u8_offset as usize)) { 132 u8_offset += length.length_utf8 as u32; 133 u16_offset += length.length_utf16 as u32; 134 } else { 135 u16_offset += 1; 136 u8_offset += 1; 137 } 138 } 139 140 u8_offset 141 } 142 143 /// Checks if the given span spans an entire line (excluding the newline 144 /// character itself). 145 pub fn spans_entire_line(&self, span: &SrcSpan) -> bool { 146 self.line_starts.iter().any(|&line_start| { 147 line_start == span.start && self.line_starts.contains(&(span.end + 1)) 148 }) 149 } 150} 151 152#[test] 153fn byte_index() { 154 let src = r#"import gleam/io 155 156pub fn main() { 157 io.println("Hello, world!") 158} 159"#; 160 let line_numbers = LineNumbers::new(src); 161 162 assert_eq!( 163 line_numbers.byte_index(Position { 164 line: 0, 165 character: 0 166 }), 167 0 168 ); 169 assert_eq!( 170 line_numbers.byte_index(Position { 171 line: 0, 172 character: 4 173 }), 174 4 175 ); 176 assert_eq!( 177 line_numbers.byte_index(Position { 178 line: 100, 179 character: 0 180 }), 181 src.len() as u32 182 ); 183 assert_eq!( 184 line_numbers.byte_index(Position { 185 line: 2, 186 character: 1 187 }), 188 18 189 ); 190} 191 192// https://github.com/gleam-lang/gleam/issues/3628 193#[test] 194fn byte_index_with_multibyte_characters() { 195 let src = r#"fn wibble(_a, _b, _c) { 196 todo 197} 198 199pub fn main() { 200 wibble("क्षि", 10, <<"abc">>) 201} 202"#; 203 let line_numbers = LineNumbers::new(src); 204 205 assert_eq!( 206 line_numbers.byte_index(Position { 207 line: 1, 208 character: 6 209 }), 210 30 211 ); 212 assert_eq!( 213 line_numbers.byte_index(Position { 214 line: 5, 215 character: 2 216 }), 217 52 218 ); 219 assert_eq!( 220 line_numbers.byte_index(Position { 221 line: 5, 222 character: 17 223 }), 224 75 225 ); 226 assert_eq!( 227 line_numbers.byte_index(Position { 228 line: 6, 229 character: 1 230 }), 231 91 232 ); 233} 234 235// https://github.com/gleam-lang/gleam/issues/3628 236#[test] 237fn line_and_column_with_multibyte_characters() { 238 let src = r#"fn wibble(_a, _b, _c) { 239 todo 240} 241 242pub fn main() { 243 wibble("क्षि", 10, <<"abc">>) 244} 245"#; 246 let line_numbers = LineNumbers::new(src); 247 248 assert_eq!( 249 line_numbers.line_and_column_number(30), 250 LineColumn { line: 2, column: 7 } 251 ); 252 assert_eq!( 253 line_numbers.line_and_column_number(52), 254 LineColumn { line: 6, column: 3 } 255 ); 256 assert_eq!( 257 line_numbers.line_and_column_number(75), 258 LineColumn { 259 line: 6, 260 column: 18 261 } 262 ); 263 assert_eq!( 264 line_numbers.line_and_column_number(91), 265 LineColumn { line: 7, column: 2 } 266 ); 267} 268 269/// A 1-index line and column position 270#[derive(Debug, Clone, Copy, PartialEq, Eq)] 271pub struct LineColumn { 272 pub line: u32, 273 pub column: u32, 274}