Skip to main content

a2kit/lang/
mod.rs

1//! # Language Module
2//! 
3//! This module contains facilities for language transformations and analysis.
4//! The root module `lang` contains code for navigating any syntax tree, and
5//! general code for interacting with the CLI or the language servers.
6//! 
7//! The syntax trees are generated using Tree-sitter parsers, which reside in their own crates.
8//! 
9//! The submodules contain the specific language transformations and analysis.
10//!
11//! The language servers themselves are compiled to separate executables, and as
12//! such, per rust convention, are in src/bin.  In particular, communication with a
13//! language client is handled there, not here.
14
15pub mod applesoft;
16pub mod integer;
17pub mod merlin;
18mod linenum;
19pub mod server;
20pub mod disk_server;
21
22use tree_sitter;
23use lsp_types as lsp;
24use colored::*;
25use thiserror::Error;
26use std::{collections::HashSet, io};
27use std::io::Write;
28use num_traits::Num;
29use std::str::FromStr;
30use std::collections::BTreeMap;
31use atty;
32
33use crate::{STDRESULT,DYNERR};
34const RCH: &str = "unreachable was reached";
35
36pub enum Navigation {
37    GotoSelf,
38    GotoChild,
39    GotoSibling,
40    GotoParentSibling,
41    Descend,
42    Exit,
43    Abort
44}
45
46#[derive(Error,Debug)]
47pub enum Error {
48    #[error("Syntax error")]
49    Syntax,
50    #[error("Invalid Line Number")]
51    LineNumber,
52    #[error("Tokenization error")]
53    Tokenization,
54    #[error("Detokenization error")]
55    Detokenization,
56    #[error("Parsing error")]
57    ParsingError,
58    #[error("Path not found")]
59    PathNotFound,
60    #[error("Out of range")]
61    OutOfRange,
62    #[error("Could not parse URL")]
63    BadUrl
64}
65
66/// Take a URI from the client and recreate it using the server's conventions.
67/// This is needed in order to make reliable file system comparisons.
68/// If the URI cannot be interpreted by `std::path` the input is returned unchanged.
69pub fn normalize_client_uri(uri: lsp::Uri) -> lsp::Uri {
70    match pathbuf_from_uri(&uri) {
71        Ok(path) => match uri_from_path(&path) {
72            Ok(ans) => return ans,
73            Err(_) => {}
74        },
75        Err(_) => {}
76    }
77    uri
78}
79
80/// Convenience function calling `normalize_client_uri`
81pub fn normalize_client_uri_str(uri: &str) -> Result<lsp::Uri,DYNERR> {
82    Ok(normalize_client_uri(lsp::Uri::from_str(uri)?))
83}
84
85pub fn uri_from_path(path: &std::path::Path) -> Result<lsp::Uri,DYNERR> {
86    let url = match url::Url::from_file_path(path) {
87        Ok(ans) => ans,
88        Err(()) => return Err(Box::new(Error::PathNotFound))
89    };
90    let uri = fluent_uri::Uri::from_str(url.as_str())?.normalize();
91    Ok(lsp::Uri::from_str(uri.as_str())?)
92}
93
94/// N.b. `path_str` should be a full path
95pub fn uri_from_path_str(path_str: &str) -> Result<lsp::Uri,DYNERR> {
96    uri_from_path(&std::path::PathBuf::from_str(path_str)?)
97}
98
99pub fn pathbuf_from_uri(uri: &lsp::Uri) -> Result<std::path::PathBuf,DYNERR> {
100    let url_crate_uri = match url::Url::from_str(uri.as_str()) {
101        Ok(ans) => ans,
102        Err(e) => return Err(Box::new(e))
103    };
104    match url_crate_uri.to_file_path() {
105        Ok(ans) => Ok(ans),
106        Err(_) => Err(Box::new(Error::BadUrl))
107    }
108}
109
110/// Return a value indicating the quality of the match of an emulation path to a document in the
111/// host file system.  Any value >0 means the filename itself matched case insensitively.
112/// Higher values mean there were additional matches, such as parent directories.
113fn match_emulation_path(emu_path: &str, sep: &str, doc: &Document) -> usize {
114    let mut quality = 0;
115    let Some(scheme) = doc.uri.scheme() else { return quality; };
116    if scheme.as_str() != "file" { return quality; }
117    let Ok(doc_path) = crate::lang::pathbuf_from_uri(&doc.uri) else {
118        log::trace!("error while parsing {}",doc.uri.as_str());
119        return quality;
120    };
121    let mut doc_segs = doc_path.iter().rev();
122    let emu_segs = emu_path.split(sep).map(|x| x.to_string()).collect::<Vec<String>>();
123    for emu_seg in emu_segs.iter().rev() {
124        if let Some(doc_seg) = doc_segs.next() {
125            if let Some(s) = doc_seg.to_str() {
126                if s.to_lowercase() == emu_seg.to_lowercase() {
127                    quality += 1;
128                } else {
129                    break;
130                }
131            }
132        }
133    }
134    quality
135}
136
137/// Get the document URI from the given set that is the best match to the given emulation path.
138/// This may return an empty set, or a set with more than one match, where the latter
139/// means there were multiple equally good matches.  It is OK for the document vector to contain
140/// duplicate URI (the returned Vec is formed from a HashSet).
141fn get_emulation_match(docs: &Vec<Document>, emu_path: &str, sep: &str) -> Vec<lsp::Uri> {
142    let mut set = HashSet::new();
143    let mut best_quality = 0;
144    let mut shortest = usize::MAX; // secondary quality measure
145    log::debug!("search for file `{}`",emu_path);
146    for doc in docs {
147        let quality = match_emulation_path(&emu_path, sep, &doc);
148        let l = doc.uri.as_str().len();
149        log::trace!("match {} to {} Q={}",emu_path,doc.uri.as_str(),quality);
150        if quality > best_quality {
151            set = HashSet::new();
152            set.insert(doc.uri.clone());
153            best_quality = quality;
154            shortest = l;
155        } else if quality > 0 && quality == best_quality {
156            if l < shortest {
157                set = HashSet::new();
158                set.insert(doc.uri.clone());
159                shortest = l;
160            } else if l == shortest {
161                set.insert(doc.uri.clone());
162            }
163        }
164    }
165    log::debug!("found {} URI candidates",set.len());
166    let mut ans = Vec::new();
167    for uri in &set {
168        log::trace!("  {}",uri.as_str());
169        ans.push(uri.clone())
170    }
171    ans
172    //ans.iter().map(|x| x.clone()).collect()
173}
174
175/// Text document packed up with URI string and version information.
176/// This is similar to the LSP `TextDocumentItem`, except that it originates
177/// on the server side, or from the CLI.
178/// There are internally defined URI's for strings and macros.
179#[derive(Clone)]
180pub struct Document {
181    pub uri: lsp::Uri,
182    pub version: Option<i32>,
183    pub text: String
184}
185
186impl Document {
187    pub fn new(uri: lsp::Uri,text: String) -> Self {
188        Self {
189            uri,
190            version: None,
191            text
192        }
193    }
194    pub fn from_string(text: String, id: u64) -> Self {
195        Self {
196            uri: lsp::Uri::from_str(&format!("string:{}",id)).expect(RCH),
197            version: None,
198            text
199        }
200    }
201    pub fn from_macro(text: String, label: String) -> Self {
202        Self {
203            uri: lsp::Uri::from_str(&format!("macro:{}",label)).expect(RCH),
204            version: None,
205            text
206        }
207    }
208    pub fn from_file_path(path: &std::path::Path) -> Result<Self,DYNERR> {
209        let by = std::fs::read(path)?;
210        Ok(Self {
211            uri: uri_from_path(path)?,
212            version: None,
213            text: String::from_utf8(by)?
214        })
215    }
216}
217
218pub fn range_contains_pos(rng: &lsp::Range, pos: &lsp::Position) -> bool
219{
220	if pos.line < rng.start.line || pos.line > rng.end.line {
221		return false;
222    }
223	if pos.line == rng.start.line && pos.character < rng.start.character {
224		return false;
225    }
226	if pos.line == rng.end.line && pos.character > rng.end.character {
227		return false;
228    }
229	return true;
230}
231
232pub fn range_contains_range(outer: &lsp::Range, inner: &lsp::Range) -> bool
233{
234	if inner.start.line < outer.start.line || inner.end.line > outer.end.line {
235		return false;
236    }
237	if inner.start.line == outer.start.line && inner.start.character < outer.start.character {
238		return false;
239    }
240	if inner.end.line == outer.end.line && inner.end.character > outer.end.character {
241		return false;
242    }
243	return true;
244}
245
246pub fn translate_pos(pos: &lsp::Position, dl: isize, dc: isize) -> lsp::Position {
247    let mut ans = lsp::Position::new(0,0);
248    ans.line = match pos.line as isize + dl < 0 {
249        true => 0,
250        false => (pos.line as isize + dl) as u32
251    };
252    ans.character = match pos.character as isize + dc < 0 {
253        true => 0,
254        false => (pos.character as isize + dc) as u32
255    };
256    ans
257}
258
259pub fn range_union(r1: &lsp::Range,r2: &lsp::Range) -> lsp::Range {
260    lsp::Range::new(
261        lsp::Position::new(
262            match r1.start.line < r2.start.line { true => r1.start.line, false => r2.start.line },
263            match r1.start.line < r2.start.line || r1.start.line == r2.start.line && r1.start.character < r2.start.character {
264                true => r1.start.character,
265                false => r2.start.character
266            }
267        ),
268        lsp::Position::new(
269            match r2.end.line > r1.end.line { true => r2.end.line, false => r1.end.line },
270            match r2.end.line > r1.end.line || r2.end.line == r1.end.line && r2.end.character > r1.end.character {
271                true => r2.end.character,
272                false => r1.end.character
273            }
274        )
275    )
276}
277
278/// Take a range from a tree-sitter parser and convert it to an LSP range.
279/// The `row` argument is used when we are parsing line by line.
280/// The `col` argument is only needed to subtract out parsing hints.
281pub fn lsp_range(rng: tree_sitter::Range,row: isize,col: isize) -> lsp::Range {
282    lsp::Range {
283        start: lsp::Position { line: (row + rng.start_point.row as isize) as u32, character: (col + rng.start_point.column as isize) as u32 },
284        end: lsp::Position { line: (row + rng.end_point.row as isize) as u32, character: (col + rng.end_point.column as isize) as u32}
285    }
286}
287
288/// Get text of the node, returning null string if there is any error
289pub fn node_text(node: &tree_sitter::Node,source: &str) -> String {
290    if let Ok(ans) = node.utf8_text(source.as_bytes()) {
291        return ans.to_string();
292    }
293    return "".to_string();
294}
295
296/// Parse a node that is expected to be an integer literal and put into generic type,
297/// if node cannot be parsed return None.  This will ignore all spaces.
298/// Actually this will work for floating point types as well.
299pub fn node_integer<T: FromStr>(node: &tree_sitter::Node,source: &str) -> Option<T> {
300    let txt = node_text(&node,source).replace(" ","");
301    match txt.parse::<T>() {
302        Ok(num) => Some(num),
303        Err(_) => None
304    }
305}
306
307/// Parse a node that may use a prefix to indicate radix, e.g., `$0F` or `%00001111`.
308/// This will ignore all spaces and underscores, except for an underscore prefix.
309pub fn node_radix<T: Num>(node: &tree_sitter::Node, source: &str, hex: &str, bin: &str) -> Option<T> {
310    if let Ok(s) = node.utf8_text(source.as_bytes()) {
311        let mut trimmed = s.to_string().replace(" ","").replace("_","");
312        if s.starts_with("_") && (hex=="_" || bin=="_") {
313            trimmed = ["_",&trimmed].concat();
314        }
315        if trimmed.starts_with(hex) {
316            match T::from_str_radix(&trimmed[1..],16) {
317                Ok(ans) => Some(ans),
318                Err(_) => None
319            }
320        } else if trimmed.starts_with(bin) {
321            match T::from_str_radix(&trimmed[1..],2) {
322                Ok(ans) => Some(ans),
323                Err(_) => None
324            }
325        } else {
326            match T::from_str_radix(&trimmed,10) {
327                Ok(ans) => Some(ans),
328                Err(_) => None
329            }
330        }
331    } else {
332        None
333    }
334}
335
336pub fn named_sibling(node: tree_sitter::Node,skip: usize) -> Option<tree_sitter::Node> {
337
338    let mut maybe = Some(node);
339    for _i in 0..skip {
340        maybe = maybe.unwrap().next_named_sibling();
341        if maybe.is_none() {
342            return None;
343        }
344    }
345    maybe
346}
347
348/// Extend a zero range one code point in either direction, if possible.
349/// Finite ranges are not modified.
350/// The byte range is not modified in any case.
351pub fn extended_range(node: &tree_sitter::Node,end_col: usize) -> tree_sitter::Range {
352    let mut ans = node.range();
353    if ans.start_point.column == ans.end_point.column {
354        if ans.start_point.column > 0 {
355            ans.start_point.column -= 1;
356        }
357        if ans.end_point.column + 1 < end_col {
358            ans.end_point.column += 1;
359        }
360    }
361    ans
362}
363
364/// Try to update a named boolean from a serde `Value` presumed to be an object.
365/// If there is any error do not change the value.
366pub fn update_json_bool(maybe_obj: &serde_json::Value, key: &str, curr: &mut bool) {
367    if let Some(outer) = maybe_obj.as_object() {
368        if let Some(x) = outer.get(key) {
369            match x.as_bool() { Some(x) => *curr = x, _ => {} };
370        }
371    }
372}
373
374/// Try to update a named integer from a serde `Value` presumed to be an object.
375/// If there is any error do not change the value.
376pub fn update_json_i64(maybe_obj: &serde_json::Value, key: &str, curr: &mut i64) {
377    if let Some(outer) = maybe_obj.as_object() {
378        if let Some(x) = outer.get(key) {
379            match x.as_i64() { Some(x) => *curr = x, _ => {} };
380        }
381    }
382}
383
384/// Try to update a named float from a serde `Value` presumed to be an object.
385/// If there is any error do not change the value.
386pub fn update_json_f64(maybe_obj: &serde_json::Value, key: &str, curr: &mut f64) {
387    if let Some(outer) = maybe_obj.as_object() {
388        if let Some(x) = outer.get(key) {
389            match x.as_f64() { Some(x) => *curr = x, _ => {} };
390        }
391    }
392}
393
394/// Try to update a named optional string from a serde `Value` presumed to be an object.
395/// If there is any error do not change the value.
396pub fn update_json_string_opt(maybe_obj: &serde_json::Value, key: &str, curr: &mut Option<String>) {
397    if let Some(outer) = maybe_obj.as_object() {
398        if let Some(x) = outer.get(key) {
399            match x.as_str() { Some(x) => *curr = Some(x.to_string()), _ => {} };
400        }
401    }
402}
403
404/// Try to update a named string from a serde `Value` presumed to be an object.
405/// If there is any error do not change the value.
406pub fn update_json_string(maybe_obj: &serde_json::Value, key: &str, curr: &mut String) {
407    if let Some(outer) = maybe_obj.as_object() {
408        if let Some(x) = outer.get(key) {
409            match x.as_str() { Some(x) => *curr = x.to_string(), _ => {} };
410        }
411    }
412}
413
414/// Try to update a named severity from a serde `Value` presumed to be an object.
415/// If there is any error do not change the value.
416pub fn update_json_severity(maybe_obj: &serde_json::Value, key: &str, curr: &mut Option<lsp::DiagnosticSeverity>) {
417    if let Some(outer) = maybe_obj.as_object() {
418        if let Some(x) = outer.get(key) {
419            match x.as_str() {
420                Some("ignore") => *curr = None,
421                Some("hint") => *curr = Some(lsp::DiagnosticSeverity::HINT),
422                Some("info") => *curr = Some(lsp::DiagnosticSeverity::INFORMATION),
423                Some("warn") => *curr = Some(lsp::DiagnosticSeverity::WARNING),
424                Some("error") => *curr = Some(lsp::DiagnosticSeverity::ERROR),
425                _ => {}
426            }
427        }
428    }
429}
430
431/// Try to update a named list of integers from a serde `Value` presumed to be an object.
432/// If there is any error do not change the value.
433pub fn update_json_vec(maybe_obj: &serde_json::Value, key: &str, curr: &mut Vec<i64>) {
434    if let Some(outer) = maybe_obj.as_object() {
435        if let Some(x) = outer.get(key) {
436            let mut ans: Vec<i64> = Vec::new();
437            if let Some(a) = x.as_array() {
438                for v in a {
439                    match v.as_i64() {
440                        Some(i) => ans.push(i),
441                        None => return
442                    }
443                }
444                *curr = ans;
445            }
446        }
447    }
448}
449
450/// Try to update a named list of strings from a serde `Value` presumed to be an object.
451/// If there is any error do not change the value.
452pub fn update_json_vec_str(maybe_obj: &serde_json::Value, key: &str, curr: &mut Vec<String>) {
453    if let Some(outer) = maybe_obj.as_object() {
454        if let Some(x) = outer.get(key) {
455            let mut ans: Vec<String> = Vec::new();
456            if let Some(a) = x.as_array() {
457                for v in a {
458                    match v.as_str() {
459                        Some(s) => ans.push(s.to_owned()),
460                        None => return
461                    }
462                }
463                *curr = ans;
464            }
465        }
466    }
467}
468
469/// Trait for navigating a syntax tree in any language.
470pub trait Navigate {
471    fn visit(&mut self,curs: &tree_sitter::TreeCursor) -> Result<Navigation,DYNERR>;
472    fn descend(&mut self,_curs: &tree_sitter::TreeCursor) -> Result<Navigation,DYNERR> {
473        Ok(Navigation::GotoSibling)
474    }
475    fn walk(&mut self,tree: &tree_sitter::Tree) -> Result<(),DYNERR>
476    {
477        let mut curs = tree.walk();
478        let mut choice = Navigation::GotoSelf;
479        while ! matches!(choice,Navigation::Exit | Navigation::Abort)
480        {
481            if matches!(choice,Navigation::GotoSelf) {
482                choice = self.visit(&curs)?;
483            } else if matches!(choice,Navigation::Descend) {
484                choice = self.descend(&curs)?;
485            } else if matches!(choice,Navigation::GotoChild) && curs.goto_first_child() {
486                choice = self.visit(&curs)?;
487            } else if matches!(choice,Navigation::GotoParentSibling) && curs.goto_parent() && curs.goto_next_sibling() {
488                choice = self.visit(&curs)?;
489            } else if matches!(choice,Navigation::GotoSibling) && curs.goto_next_sibling() {
490                choice = self.visit(&curs)?;
491            } else if curs.goto_next_sibling() {
492                choice = self.visit(&curs)?;
493            } else if curs.goto_parent() {
494                choice = Navigation::GotoSibling;
495            } else {
496                choice = Navigation::Exit;
497            }
498        }
499        Ok(())
500    }
501}
502
503
504/// Test for the given language, returns false if there is any syntax error,
505/// does not always return true otherwise (additional criteria may be used).
506/// Warnings may be emitted if the results are ambiguous.
507/// Works for any language, provided it is line-oriented.
508pub fn is_lang(lang: tree_sitter::Language,code: &str) -> bool {
509    let mut parser = tree_sitter::Parser::new();
510    parser.set_language(&lang).expect("language not found");
511    let mut iter = code.lines();
512    let mut line_count = 0;
513    let mut good_lines = 0;
514    let mut great_lines = 0;
515    while let Some(line) = iter.next()
516    {
517        line_count += 1;
518        if let Some(tree) = parser.parse(String::from(line) + "\n",None) {
519            let curs = tree.walk();
520            if !curs.node().has_error() {
521                good_lines += 1;
522                match lang.name() {
523                    Some("merlin6502") => {
524                        match curs.node().child(0) {
525                            Some(child) => match child.kind() {
526                                "operation" => great_lines += 1,
527                                "pseudo_operation" => great_lines += 1,
528                                _ => {}
529                            },
530                            None => {}
531                        }
532                    },
533                    _ => great_lines += 1
534                }
535            }
536        }
537    }
538    if line_count == 0 {
539        log::warn!("encountered empty file");
540        return false;
541    }
542    if (great_lines==0 || good_lines != line_count) && good_lines * 3 > line_count {
543        log::warn!("{} {} parsed as {} but merit test failed",good_lines,match good_lines==1 { true => "line", false => "lines" },lang.name().unwrap_or("unknown"));
544    }
545    good_lines == line_count && great_lines > 0
546}
547
548/// Simple verify, returns an error if syntax check fails, but does not run full diagnostics.
549/// This is used by the CLI to interrupt the pipeline when a bad language file is encountered.
550/// Works for any language, provided it is line-oriented.
551pub fn verify_str(lang: tree_sitter::Language,code: &str) -> STDRESULT {
552    let mut parser = tree_sitter::Parser::new();
553    parser.set_language(&lang)?;
554    let mut iter = code.lines();
555    let mut row = 0;
556    while let Some(line) = iter.next()
557    {
558        match parser.parse(String::from(line) + "\n",None) {
559            Some(tree) => {
560                let curs = tree.walk();
561                if curs.node().has_error() {
562                    log::error!("syntax error in row {}, use `verify` for more details",row);
563                    return Err(Box::new(Error::Syntax));
564                }
565            },
566            None => {
567                log::error!("unable to parse row {}",row);
568                return Err(Box::new(Error::Syntax));
569            }
570        }
571        row += 1;
572    }
573    Ok(())
574}
575
576pub fn eprint_lines_sexpr(lang: tree_sitter::Language, program: &str, unwraps: usize) {
577    let mut parser = tree_sitter::Parser::new();
578    parser.set_language(&lang).expect("Error loading grammar");
579    let mut iter = program.lines();
580    eprintln!();
581    while let Some(line) = iter.next()
582    {
583        if let Some(tree) = parser.parse(String::from(line) + "\n",None) {
584            let mut curs = tree.walk();
585            for _i in 0..unwraps {
586                curs.goto_first_child();
587            }
588            eprintln!("{}",line.to_string());
589            eprintln!("{}",curs.node().to_sexp());
590        }
591    }
592}
593
594/// Gather program lines from the console, panics if stdin is not the console
595pub fn line_entry(lang: tree_sitter::Language,prompt: &str) -> String
596{
597    let mut parser = tree_sitter::Parser::new();
598    parser.set_language(&lang).expect("Error loading grammar");
599    let mut code = String::new();
600    if atty::is(atty::Stream::Stdin) {
601        eprintln!("Line entry interface.");
602        eprintln!("This is a blind accumulation of lines.");
603        eprintln!("Verify occurs when entry is terminated.");
604        eprintln!("Accumulated lines can be piped.");
605        eprintln!("`bye` terminates.");
606        loop {
607            eprint!("{} ",prompt);
608            let mut line = String::new();
609            io::stderr().flush().expect("could not flush stderr");
610            io::stdin().read_line(&mut line).expect("could not read stdin");
611            if line=="bye\n" || line=="bye\r\n" {
612                break;
613            }
614            code += &line;
615        }
616        return code;
617    } else {
618        panic!("line_entry was called with piped input");
619    }
620}
621
622pub fn eprint_diagnostic(diag: &lsp::Diagnostic, program: &str) {
623    // line search not very efficient, perhaps it will do...
624    if let Some(sev) = diag.severity {
625        if sev == lsp::DiagnosticSeverity::HINT {
626            // at present this is used to dim conditional assembly,
627            // and we don't want to flag it.
628            return;
629        }
630    }
631    let mut lines = program.lines();
632    let mut maybe_line = None;
633    for _i in 0..diag.range.start.line+1 {
634        maybe_line = lines.next();
635    }
636    let [announcement,squiggle] = match diag.severity {
637        Some(lsp::DiagnosticSeverity::ERROR) => ["Error".red(),"^".red()],
638        Some(lsp::DiagnosticSeverity::WARNING) => ["Warning".bright_yellow(),"^".bright_yellow()],
639        Some(lsp::DiagnosticSeverity::INFORMATION) => ["Information".bright_blue(),"^".bright_blue()],
640        _ => ["Unexpected Notice".red(),"^".red()]
641    };
642    eprintln!("{} on line {}: {}",announcement,diag.range.start.line,diag.message);
643    if let Some(line) = maybe_line {
644        eprintln!("  {}",line);
645        for _i in 0..diag.range.start.character+2 {
646            eprint!(" ");
647        }
648        for _i in diag.range.start.character..diag.range.end.character {
649            eprint!("{}",squiggle);
650        }    
651        eprintln!();
652    } 
653}
654
655/// This assumes all CRLF have been filtered from `doc`.
656/// CRLF in `raw_new` will be changed to LF.
657fn replace_range(doc: &mut String, rng: lsp::Range, raw_new: &str) -> STDRESULT {
658    let new = raw_new.replace("\r\n","\n");
659    let mut start_char = 0;
660    let mut end_char = 0;
661    let mut curr_line = 0;
662    let mut found_start = false;
663    let mut found_end = false;
664    for line in doc.lines() {
665        if rng.start.line == curr_line {
666            start_char += rng.start.character;
667            found_start = true;
668        }
669        if !found_start {
670            start_char += line.chars().count() as u32 + 1;
671        }
672        if rng.end.line == curr_line {
673            end_char += rng.end.character;
674            found_end = true;
675            break;
676        }
677        if !found_end {
678            end_char += line.chars().count() as u32 + 1;
679        }
680        curr_line += 1;
681    }
682    if found_start && found_end {
683        doc.replace_range(start_char as usize..end_char as usize,&new);
684        return Ok(());
685    }
686    // there still could be an insertion at the end
687    let line_count = doc.lines().count() as u32;
688    if rng.start.line==line_count && rng.start.character==0 && rng.end.line==line_count && rng.end.character==0 {
689        doc.push_str(&new);
690        return Ok(());
691    }
692    Err(Box::new(Error::LineNumber))
693}
694
695/// Strategy is to sort edits bottom to top and apply in that sequence, this way the
696/// meaning of a row doesn't change as we make the replacements.  Overlaps not allowed.
697/// This is consistent with the LSP.  Preserves CRLF or LF, unless there is a mixture,
698/// in which case LF wins.  Panics if row is out of range.
699pub fn apply_edits(doc: &str, edits: &Vec<lsp::TextEdit>, row: u32) -> Result<String,DYNERR> {
700    // TODO: check for overlaps
701    let line_sep = match doc.split("\r\n").count() == doc.split("\n").count() {
702        true => "\r\n",
703        false => "\n"
704    };
705    let mut ans = String::from(doc);
706    ans = ans.replace("\r\n","\n");
707    let mut sorted = BTreeMap::new();
708    let mut idx: u32 = 0; // provide uniqueness in case of repeated insertions or deletions
709    for edit in edits {
710        let key = (edit.range.start.line,edit.range.start.character,idx);
711        sorted.insert(key,edit.clone());
712        idx += 1;
713    }
714    for edit in sorted.values().rev() {
715        let offset_rng = lsp::Range::new(
716            lsp::Position::new(edit.range.start.line - row,edit.range.start.character),
717            lsp::Position::new(edit.range.end.line - row,edit.range.end.character)
718        );
719        log::trace!("replace {:?}",offset_rng);
720        replace_range(&mut ans,offset_rng,&edit.new_text)?;
721    }
722    if line_sep == "\r\n" {
723        ans = ans.replace("\n","\r\n");
724    }
725    Ok(ans)
726}