Skip to main content

blues_lsp/project/
file.rs

1use std::{fs, ops::Range, path::PathBuf, sync::Arc};
2
3use arcstr::{ArcStr, Substr};
4use tracing::{error, trace};
5
6use crate::syntax::{
7    TextChunk,
8    location::{FilePos, LineColCounter, LspCol, OriginId, Pos, ROOT_ORIGIN},
9};
10
11use super::{FileHandle, PkgHandle, Project, querry::Queryable};
12
13pub struct File {
14    pub path: Option<PathBuf>,
15}
16
17impl File {
18    pub fn new(path: Option<PathBuf>) -> Self {
19        Self { path }
20    }
21}
22
23/// File contents, some precomputed data
24#[derive(Debug)]
25pub struct FileContents {
26    pub contents: ArcStr,
27    /// For each line in file:
28    /// 0 - byte offstet to start of line
29    /// 1 - Length of line, in UTF16 units
30    line_lookup: Vec<(u32, LspCol)>,
31    end_pos: FilePos,
32}
33
34impl Queryable for FileContents {
35    type Input = FileHandle;
36    type Ctx = ();
37    type Backref = PkgHandle;
38
39    fn compute(input: Self::Input, _ctx: &Self::Ctx, proj: &Project) -> Arc<Self> {
40        if let Some(file) = proj.content_overlay.get(&input) {
41            return file.clone();
42        }
43
44        let file = proj.get_file(input);
45        let Some(path) = file.path.as_ref() else {
46            error!("FileContents::compute called on FileHandle without local path");
47            return Arc::new(FileContents::new("".into()));
48        };
49
50        trace!("reading source file '{}'", path.display());
51        let text = match fs::read_to_string(path) {
52            Ok(text) => text,
53            Err(e) => {
54                error!("could not open file {}: {e}", path.display());
55                return Arc::new(FileContents::new("".into()));
56            }
57        };
58
59        Arc::new(FileContents::new(text.into()))
60    }
61
62    fn invalidate(&self, _input: &Self::Input, _ctx: &Self::Ctx, _proj: &Project) {}
63    fn invalidate_backref(&self, backref: Self::Backref, proj: &Project) {
64        proj.syntax.invalidate(proj, &backref);
65    }
66}
67
68impl FileContents {
69    pub fn new(contents: ArcStr) -> Self {
70        let (line_lookup, end_pos) = Self::gen_line_lookup(&contents);
71
72        FileContents {
73            contents,
74            line_lookup,
75            end_pos,
76        }
77    }
78
79    fn gen_line_lookup(text: &str) -> (Vec<(u32, LspCol)>, FilePos) {
80        let mut line_ofs = 0;
81        let mut cols = LspCol::start();
82        let mut line = 0;
83        let mut counter = LineColCounter::default();
84        let mut out = Vec::new();
85
86        for (ofs, c) in text.char_indices() {
87            if counter.pos(Some(c)).line != line {
88                out.push((line_ofs, cols));
89                line_ofs = ofs as u32;
90                line += 1;
91                cols = LspCol::start();
92            }
93            cols.advance(c);
94            counter.advance(c);
95        }
96
97        out.push((line_ofs, cols));
98
99        // Handle trailing empty line
100        if counter.pos(None).col == LspCol::start() {
101            out.push((text.len() as u32, LspCol::start()));
102        }
103
104        (out, counter.pos(None))
105    }
106
107    pub fn pos_add(&self, start: FilePos, ofs: LspCol) -> FilePos {
108        let mut line = start.line;
109        let mut col = start.col.to_raw() + ofs.to_raw();
110
111        while col > self.line_width(line) {
112            col -= self.line_width(line);
113            line += 1;
114        }
115
116        FilePos {
117            line,
118            col: LspCol::from_raw(col),
119        }
120    }
121
122    pub fn pos_diff(&self, range: Range<FilePos>) -> LspCol {
123        let diff = (range.start.line..range.end.line)
124            .map(|l| self.line_width(l))
125            .sum::<u32>()
126            + range.end.col.to_raw()
127            - range.start.col.to_raw();
128
129        LspCol::from_raw(diff)
130    }
131
132    pub fn line_width(&self, line: u32) -> u32 {
133        self.line_lookup[line as usize].1.to_raw()
134    }
135
136    pub fn chunks(&self, origin: OriginId) -> Vec<TextChunk> {
137        if self.contents.is_empty() {
138            Vec::new()
139        } else {
140            vec![TextChunk {
141                pos: Pos::at(origin, 0, 0),
142                text: Substr::full(self.contents.clone()),
143            }]
144        }
145    }
146
147    pub fn end_pos(&self) -> Pos {
148        Pos {
149            origin: ROOT_ORIGIN,
150            text_pos: self.end_pos,
151        }
152    }
153}