blues-lsp 0.1.0

LSP language server for the Bluespec SystemVerilog language
Documentation
use std::{fs, ops::Range, path::PathBuf, sync::Arc};

use arcstr::{ArcStr, Substr};
use tracing::{error, trace};

use crate::syntax::{
    TextChunk,
    location::{FilePos, LineColCounter, LspCol, OriginId, Pos, ROOT_ORIGIN},
};

use super::{FileHandle, PkgHandle, Project, querry::Queryable};

pub struct File {
    pub path: Option<PathBuf>,
}

impl File {
    pub fn new(path: Option<PathBuf>) -> Self {
        Self { path }
    }
}

/// File contents, some precomputed data
#[derive(Debug)]
pub struct FileContents {
    pub contents: ArcStr,
    /// For each line in file:
    /// 0 - byte offstet to start of line
    /// 1 - Length of line, in UTF16 units
    line_lookup: Vec<(u32, LspCol)>,
    end_pos: FilePos,
}

impl Queryable for FileContents {
    type Input = FileHandle;
    type Ctx = ();
    type Backref = PkgHandle;

    fn compute(input: Self::Input, _ctx: &Self::Ctx, proj: &Project) -> Arc<Self> {
        if let Some(file) = proj.content_overlay.get(&input) {
            return file.clone();
        }

        let file = proj.get_file(input);
        let Some(path) = file.path.as_ref() else {
            error!("FileContents::compute called on FileHandle without local path");
            return Arc::new(FileContents::new("".into()));
        };

        trace!("reading source file '{}'", path.display());
        let text = match fs::read_to_string(path) {
            Ok(text) => text,
            Err(e) => {
                error!("could not open file {}: {e}", path.display());
                return Arc::new(FileContents::new("".into()));
            }
        };

        Arc::new(FileContents::new(text.into()))
    }

    fn invalidate(&self, _input: &Self::Input, _ctx: &Self::Ctx, _proj: &Project) {}
    fn invalidate_backref(&self, backref: Self::Backref, proj: &Project) {
        proj.syntax.invalidate(proj, &backref);
    }
}

impl FileContents {
    pub fn new(contents: ArcStr) -> Self {
        let (line_lookup, end_pos) = Self::gen_line_lookup(&contents);

        FileContents {
            contents,
            line_lookup,
            end_pos,
        }
    }

    fn gen_line_lookup(text: &str) -> (Vec<(u32, LspCol)>, FilePos) {
        let mut line_ofs = 0;
        let mut cols = LspCol::start();
        let mut line = 0;
        let mut counter = LineColCounter::default();
        let mut out = Vec::new();

        for (ofs, c) in text.char_indices() {
            if counter.pos(Some(c)).line != line {
                out.push((line_ofs, cols));
                line_ofs = ofs as u32;
                line += 1;
                cols = LspCol::start();
            }
            cols.advance(c);
            counter.advance(c);
        }

        out.push((line_ofs, cols));

        // Handle trailing empty line
        if counter.pos(None).col == LspCol::start() {
            out.push((text.len() as u32, LspCol::start()));
        }

        (out, counter.pos(None))
    }

    pub fn pos_add(&self, start: FilePos, ofs: LspCol) -> FilePos {
        let mut line = start.line;
        let mut col = start.col.to_raw() + ofs.to_raw();

        while col > self.line_width(line) {
            col -= self.line_width(line);
            line += 1;
        }

        FilePos {
            line,
            col: LspCol::from_raw(col),
        }
    }

    pub fn pos_diff(&self, range: Range<FilePos>) -> LspCol {
        let diff = (range.start.line..range.end.line)
            .map(|l| self.line_width(l))
            .sum::<u32>()
            + range.end.col.to_raw()
            - range.start.col.to_raw();

        LspCol::from_raw(diff)
    }

    pub fn line_width(&self, line: u32) -> u32 {
        self.line_lookup[line as usize].1.to_raw()
    }

    pub fn chunks(&self, origin: OriginId) -> Vec<TextChunk> {
        if self.contents.is_empty() {
            Vec::new()
        } else {
            vec![TextChunk {
                pos: Pos::at(origin, 0, 0),
                text: Substr::full(self.contents.clone()),
            }]
        }
    }

    pub fn end_pos(&self) -> Pos {
        Pos {
            origin: ROOT_ORIGIN,
            text_pos: self.end_pos,
        }
    }
}