From eb5ffab1cbc010424aa1764005f71dcd67525dc1 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Sun, 15 May 2022 14:41:37 -0400 Subject: [PATCH] fix(lsp): correct positions in some scenarios (#14359) --- cli/build.rs | 13 +- cli/lsp/documents.rs | 7 + cli/lsp/text.rs | 261 ----------------------------- cli/lsp/tsc.rs | 236 +++++--------------------- cli/tests/integration/lsp_tests.rs | 12 +- cli/tsc.rs | 14 +- cli/tsc/99_main_compiler.js | 86 +++------- 7 files changed, 96 insertions(+), 533 deletions(-) diff --git a/cli/build.rs b/cli/build.rs index 019f8d02dd..e7a2da2361 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -197,6 +197,14 @@ fn create_compiler_snapshot( false } + #[op] + fn op_script_version( + _state: &mut OpState, + _args: Value, + ) -> Result, AnyError> { + Ok(Some("1".to_string())) + } + #[op] // using the same op that is used in `tsc.rs` for loading modules and reading // files, but a slightly different implementation at build time. @@ -211,7 +219,7 @@ fn create_compiler_snapshot( if args.specifier == build_specifier { Ok(json!({ "data": r#"console.log("hello deno!");"#, - "hash": "1", + "version": "1", // this corresponds to `ts.ScriptKind.TypeScript` "scriptKind": 3 })) @@ -230,7 +238,7 @@ fn create_compiler_snapshot( let data = std::fs::read_to_string(path)?; Ok(json!({ "data": data, - "hash": "1", + "version": "1", // this corresponds to `ts.ScriptKind.TypeScript` "scriptKind": 3 })) @@ -255,6 +263,7 @@ fn create_compiler_snapshot( op_cwd::decl(), op_exists::decl(), op_load::decl(), + op_script_version::decl(), ]) .state(move |state| { state.put(op_crate_libs.clone()); diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 0a383e7812..4fb8c428ee 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -200,6 +200,13 @@ impl AssetOrDocument { } } + pub fn media_type(&self) -> MediaType { + match self { + AssetOrDocument::Asset(_) => MediaType::TypeScript, // assets are always TypeScript + AssetOrDocument::Document(d) => d.media_type(), + } + } + pub fn get_maybe_dependency( &self, position: &lsp::Position, diff --git a/cli/lsp/text.rs b/cli/lsp/text.rs index 64721ebc72..33ac2048f7 100644 --- a/cli/lsp/text.rs +++ b/cli/lsp/text.rs @@ -2,13 +2,9 @@ use deno_core::error::custom_error; use deno_core::error::AnyError; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; use dissimilar::diff; use dissimilar::Chunk; use std::collections::HashMap; -use std::ops::Bound; -use std::ops::RangeBounds; use text_size::TextRange; use text_size::TextSize; use tower_lsp::jsonrpc; @@ -261,112 +257,6 @@ pub fn get_edits(a: &str, b: &str, line_index: &LineIndex) -> Vec { text_edits } -/// Convert a difference between two strings into a change range used by the -/// TypeScript Language Service. -pub fn get_range_change(a: &str, b: &str) -> Value { - if a == b { - return json!(null); - } - let chunks = diff(a, b); - let mut iter = chunks.iter().peekable(); - let mut started = false; - let mut start = 0; - let mut end = 0; - let mut new_length = 0; - let mut equal = 0; - let mut a_pos = 0; - loop { - let diff = iter.next(); - match diff { - None => break, - Some(Chunk::Equal(e)) => { - a_pos += e.encode_utf16().count(); - equal += e.encode_utf16().count(); - } - Some(Chunk::Delete(d)) => { - if !started { - start = a_pos; - started = true; - equal = 0; - } - a_pos += d.encode_utf16().count(); - if started { - end = a_pos; - new_length += equal; - equal = 0; - } - } - Some(Chunk::Insert(i)) => { - if !started { - start = a_pos; - end = a_pos; - started = true; - equal = 0; - } else { - end += equal; - } - new_length += i.encode_utf16().count() + equal; - equal = 0; - } - } - } - - json!({ - "span": { - "start": start, - "length": end - start, - }, - "newLength": new_length, - }) -} - -/// Provide a slice of a string based on a character range. -pub fn slice(s: &str, range: impl RangeBounds) -> &str { - let start = match range.start_bound() { - Bound::Included(bound) | Bound::Excluded(bound) => *bound, - Bound::Unbounded => 0, - }; - let len = match range.end_bound() { - Bound::Included(bound) => *bound + 1, - Bound::Excluded(bound) => *bound, - Bound::Unbounded => s.encode_utf16().count(), - } - start; - substring(s, start, start + len) -} - -/// Provide a substring based on the start and end character index positions. -pub fn substring(s: &str, start: usize, end: usize) -> &str { - let len = end - start; - let mut char_pos = 0; - let mut byte_start = 0; - let mut it = s.chars(); - loop { - if char_pos == start { - break; - } - if let Some(c) = it.next() { - char_pos += c.len_utf16(); - byte_start += c.len_utf8(); - } else { - break; - } - } - char_pos = 0; - let mut byte_end = byte_start; - loop { - if char_pos == len { - break; - } - if let Some(c) = it.next() { - char_pos += c.len_utf16(); - byte_end += c.len_utf8(); - } else { - break; - } - } - &s[byte_start..byte_end] -} - #[cfg(test)] mod tests { use super::*; @@ -637,155 +527,4 @@ const C: char = \"パ パ\"; ] ) } - - #[test] - fn test_get_range_change() { - let a = "abcdefg"; - let b = "abcdefg"; - let actual = get_range_change(a, b); - assert_eq!(actual, json!(null)); - - let a = "abcdefg"; - let b = "abedcfg"; - let actual = get_range_change(a, b); - assert_eq!( - actual, - json!({ - "span": { - "start": 2, - "length": 3, - }, - "newLength": 3 - }) - ); - - let a = "abfg"; - let b = "abcdefg"; - let actual = get_range_change(a, b); - assert_eq!( - actual, - json!({ - "span": { - "start": 2, - "length": 0, - }, - "newLength": 3 - }) - ); - - let a = "abcdefg"; - let b = "abfg"; - let actual = get_range_change(a, b); - assert_eq!( - actual, - json!({ - "span": { - "start": 2, - "length": 3, - }, - "newLength": 0 - }) - ); - - let a = "abcdefg"; - let b = "abfghij"; - let actual = get_range_change(a, b); - assert_eq!( - actual, - json!({ - "span": { - "start": 2, - "length": 5, - }, - "newLength": 5 - }) - ); - - let a = "abcdefghijk"; - let b = "axcxexfxixk"; - let actual = get_range_change(a, b); - assert_eq!( - actual, - json!({ - "span": { - "start": 1, - "length": 9, - }, - "newLength": 9 - }) - ); - - let a = "abcde"; - let b = "ab(c)de"; - let actual = get_range_change(a, b); - assert_eq!( - actual, - json!({ - "span" : { - "start": 2, - "length": 1, - }, - "newLength": 3 - }) - ); - - let a = "hello πŸ¦•!"; - let b = "hello deno!"; - let actual = get_range_change(a, b); - assert_eq!( - actual, - json!({ - "span": { - "start": 6, - "length": 2, - }, - "newLength": 4 - }) - ); - - let a = "hello deno!"; - let b = "hello denoπŸ¦•!"; - let actual = get_range_change(a, b); - assert_eq!( - actual, - json!({ - "span": { - "start": 10, - "length": 0, - }, - "newLength": 2 - }) - ); - - // TODO(@kitsonk): https://github.com/dtolnay/dissimilar/issues/5 - // let a = r#" πŸ¦•πŸ‡ΊπŸ‡ΈπŸ‘ "#; - // let b = r#" πŸ‡ΊπŸ‡ΈπŸ‘ "#; - // let actual = get_range_change(a, b); - // assert_eq!( - // actual, - // json!({ - // "span": { - // "start": 1, - // "length": 2, - // }, - // "newLength": 0 - // }) - // ); - } - - #[test] - fn test_substring() { - assert_eq!(substring("Deno", 1, 3), "en"); - assert_eq!(substring("yΜ†yΜ†", 2, 4), "yΜ†"); - assert_eq!(substring("πŸ¦•πŸ¦•", 2, 4), "πŸ¦•"); - } - - #[test] - fn test_slice() { - assert_eq!(slice("Deno", 1..3), "en"); - assert_eq!(slice("Deno", 1..=3), "eno"); - assert_eq!(slice("Deno Land", 1..), "eno Land"); - assert_eq!(slice("Deno", ..3), "Den"); - assert_eq!(slice("Hello πŸ¦•", 6..8), "πŸ¦•"); - } } diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 27d32b2dba..1448c8c962 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -13,7 +13,6 @@ use super::refactor::EXTRACT_INTERFACE; use super::refactor::EXTRACT_TYPE; use super::semantic_tokens; use super::semantic_tokens::SemanticTokensBuilder; -use super::text; use super::text::LineIndex; use super::urls::LspUrlMap; use super::urls::INVALID_SPECIFIER; @@ -47,7 +46,6 @@ use log::warn; use once_cell::sync::Lazy; use regex::Captures; use regex::Regex; -use std::borrow::Cow; use std::cmp; use std::collections::HashMap; use std::collections::HashSet; @@ -157,7 +155,6 @@ impl TsServer { struct AssetDocumentInner { specifier: ModuleSpecifier, text: Arc, - length: usize, line_index: Arc, maybe_navigation_tree: Option>, } @@ -173,7 +170,6 @@ impl AssetDocument { Self(Arc::new(AssetDocumentInner { specifier, text: Arc::new(text.to_string()), - length: text.encode_utf16().count(), line_index: Arc::new(LineIndex::new(text)), maybe_navigation_tree: None, })) @@ -197,14 +193,6 @@ impl AssetDocument { self.0.text.clone() } - pub fn text_str(&self) -> &str { - self.0.text.as_str() - } - - pub fn length(&self) -> usize { - self.0.length - } - pub fn line_index(&self) -> Arc { self.0.line_index.clone() } @@ -2374,17 +2362,16 @@ struct Response { data: Value, } -struct State<'a> { +struct State { last_id: usize, performance: Arc, response: Option, state_snapshot: Arc, - snapshots: HashMap<(ModuleSpecifier, Cow<'a, str>), String>, specifiers: HashMap, token: CancellationToken, } -impl<'a> State<'a> { +impl State { fn new( state_snapshot: Arc, performance: Arc, @@ -2394,7 +2381,6 @@ impl<'a> State<'a> { performance, response: None, state_snapshot, - snapshots: HashMap::default(), specifiers: HashMap::default(), token: Default::default(), } @@ -2426,31 +2412,37 @@ impl<'a> State<'a> { } ModuleSpecifier::parse(&specifier_str).map_err(|err| err.into()) } -} -/// If a snapshot is missing from the state cache, add it. -fn cache_snapshot( - state: &mut State, - specifier: &ModuleSpecifier, - version: String, -) -> Result<(), AnyError> { - if !state - .snapshots - .contains_key(&(specifier.clone(), version.clone().into())) - { - let content = state - .state_snapshot - .documents - .get(specifier) - .ok_or_else(|| { - anyhow!("Specifier unexpectedly doesn't exist: {}", specifier) - })? - .content(); - state - .snapshots - .insert((specifier.clone(), version.into()), content.to_string()); + fn get_asset_or_document( + &self, + specifier: &ModuleSpecifier, + ) -> Option { + let snapshot = &self.state_snapshot; + if specifier.scheme() == "asset" { + snapshot.assets.get(specifier).map(AssetOrDocument::Asset) + } else { + snapshot + .documents + .get(specifier) + .map(AssetOrDocument::Document) + } + } + + fn script_version(&self, specifier: &ModuleSpecifier) -> Option { + if specifier.scheme() == "asset" { + if self.state_snapshot.assets.contains_key(specifier) { + Some("1".to_string()) + } else { + None + } + } else { + self + .state_snapshot + .documents + .get(specifier) + .map(|d| d.script_version()) + } } - Ok(()) } fn normalize_specifier>( @@ -2460,28 +2452,6 @@ fn normalize_specifier>( .map_err(|err| err.into()) } -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct SourceSnapshotArgs { - specifier: String, - version: String, -} - -/// The language service is dropping a reference to a source file snapshot, and -/// we can drop our version of that document. -#[op] -fn op_dispose( - state: &mut OpState, - args: SourceSnapshotArgs, -) -> Result { - let state = state.borrow_mut::(); - let mark = state.performance.mark("op_dispose", Some(&args)); - let specifier = state.normalize_specifier(&args.specifier)?; - state.snapshots.remove(&(specifier, args.version.into())); - state.performance.measure(mark); - Ok(true) -} - #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct SpecifierArgs { @@ -2506,116 +2476,6 @@ fn op_exists(state: &mut OpState, args: SpecifierArgs) -> bool { state.state_snapshot.documents.exists(&specifier) } -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct GetChangeRangeArgs { - specifier: String, - old_length: u32, - old_version: String, - version: String, -} - -/// The language service wants to compare an old snapshot with a new snapshot to -/// determine what source has changed. -#[op] -fn op_get_change_range( - state: &mut OpState, - args: GetChangeRangeArgs, -) -> Result { - let state = state.borrow_mut::(); - let mark = state.performance.mark("op_get_change_range", Some(&args)); - let specifier = state.normalize_specifier(&args.specifier)?; - cache_snapshot(state, &specifier, args.version.clone())?; - let r = if let Some(current) = state - .snapshots - .get(&(specifier.clone(), args.version.clone().into())) - { - if let Some(prev) = state - .snapshots - .get(&(specifier, args.old_version.clone().into())) - { - Ok(text::get_range_change(prev, current)) - } else { - let new_length = current.encode_utf16().count(); - // when a local file is opened up in the editor, the compiler might - // already have a snapshot of it in memory, and will request it, but we - // now are working off in memory versions of the document, and so need - // to tell tsc to reset the whole document - Ok(json!({ - "span": { - "start": 0, - "length": args.old_length, - }, - "newLength": new_length, - })) - } - } else { - Err(custom_error( - "MissingSnapshot", - format!( - "The current snapshot version is missing.\n Args: \"{:?}\"", - args - ), - )) - }; - - state.performance.measure(mark); - r -} - -#[op] -fn op_get_length( - state: &mut OpState, - args: SourceSnapshotArgs, -) -> Result { - let state = state.borrow_mut::(); - let mark = state.performance.mark("op_get_length", Some(&args)); - let specifier = state.normalize_specifier(args.specifier)?; - let r = if let Some(asset) = state.state_snapshot.assets.get(&specifier) { - Ok(asset.length()) - } else { - cache_snapshot(state, &specifier, args.version.clone())?; - let content = state - .snapshots - .get(&(specifier, args.version.into())) - .unwrap(); - Ok(content.encode_utf16().count()) - }; - state.performance.measure(mark); - r -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct GetTextArgs { - specifier: String, - version: String, - start: usize, - end: usize, -} - -#[op] -fn op_get_text( - state: &mut OpState, - args: GetTextArgs, -) -> Result { - let state = state.borrow_mut::(); - let mark = state.performance.mark("op_get_text", Some(&args)); - let specifier = state.normalize_specifier(args.specifier)?; - let maybe_asset = state.state_snapshot.assets.get(&specifier); - let content = if let Some(content) = &maybe_asset { - content.text_str() - } else { - cache_snapshot(state, &specifier, args.version.clone())?; - state - .snapshots - .get(&(specifier, args.version.into())) - .unwrap() - }; - state.performance.measure(mark); - Ok(text::slice(content, args.start..args.end).to_string()) -} - #[op] fn op_is_cancelled(state: &mut OpState) -> bool { let state = state.borrow_mut::(); @@ -2626,13 +2486,22 @@ fn op_is_cancelled(state: &mut OpState) -> bool { fn op_load( state: &mut OpState, args: SpecifierArgs, -) -> Result, AnyError> { +) -> Result { let state = state.borrow_mut::(); let mark = state.performance.mark("op_load", Some(&args)); let specifier = state.normalize_specifier(args.specifier)?; - let document = state.state_snapshot.documents.get(&specifier); + let asset_or_document = state.get_asset_or_document(&specifier); state.performance.measure(mark); - Ok(document.map(|d| d.content().to_string())) + Ok(match asset_or_document { + Some(doc) => { + json!({ + "data": doc.text(), + "scriptKind": crate::tsc::as_ts_script_kind(&doc.media_type()), + "version": state.script_version(&specifier), + }) + } + None => Value::Null, + }) } #[op] @@ -2705,20 +2574,7 @@ fn op_script_version( // this op is very "noisy" and measuring its performance is not useful, so we // don't measure it uniquely anymore. let specifier = state.normalize_specifier(args.specifier)?; - if specifier.scheme() == "asset" { - if state.state_snapshot.assets.contains_key(&specifier) { - Ok(Some("1".to_string())) - } else { - Ok(None) - } - } else { - let script_version = state - .state_snapshot - .documents - .get(&specifier) - .map(|d| d.script_version()); - Ok(script_version) - } + Ok(state.script_version(&specifier)) } /// Create and setup a JsRuntime based on a snapshot. It is expected that the @@ -2735,11 +2591,7 @@ fn js_runtime(performance: Arc) -> JsRuntime { fn init_extension(performance: Arc) -> Extension { Extension::builder() .ops(vec![ - op_dispose::decl(), op_exists::decl(), - op_get_change_range::decl(), - op_get_length::decl(), - op_get_text::decl(), op_is_cancelled::decl(), op_load::decl(), op_resolve::decl(), diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index 08554bac89..be22830809 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -1401,7 +1401,9 @@ fn lsp_hover_change_mbc() { }, "end": { "line": 1, - "character": 13 + // the LSP uses utf16 encoded characters indexes, so + // after the deno emoiji is character index 15 + "character": 15 } }, "text": "" @@ -1425,7 +1427,7 @@ fn lsp_hover_change_mbc() { }, "position": { "line": 2, - "character": 14 + "character": 15 } }), ) @@ -1444,11 +1446,11 @@ fn lsp_hover_change_mbc() { "range": { "start": { "line": 2, - "character": 13, + "character": 15, }, "end": { "line": 2, - "character": 14, + "character": 16, }, } })) @@ -4284,7 +4286,7 @@ fn lsp_performance() { .unwrap(); assert!(maybe_err.is_none()); if let Some(res) = maybe_res { - assert_eq!(res.averages.len(), 14); + assert_eq!(res.averages.len(), 13); } else { panic!("unexpected result"); } diff --git a/cli/tsc.rs b/cli/tsc.rs index aba289d8cc..d629fb80b7 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -422,7 +422,7 @@ struct LoadArgs { specifier: String, } -fn as_ts_script_kind(media_type: &MediaType) -> i32 { +pub fn as_ts_script_kind(media_type: &MediaType) -> i32 { match media_type { MediaType::JavaScript => 1, MediaType::Jsx => 2, @@ -492,7 +492,7 @@ fn op_load(state: &mut OpState, args: Value) -> Result { }; Ok( - json!({ "data": data, "hash": hash, "scriptKind": as_ts_script_kind(&media_type) }), + json!({ "data": data, "version": hash, "scriptKind": as_ts_script_kind(&media_type) }), ) } @@ -1002,7 +1002,7 @@ mod tests { actual, json!({ "data": "console.log(\"hello deno\");\n", - "hash": "149c777056afcc973d5fcbe11421b6d5ddc57b81786765302030d7fc893bf729", + "version": "149c777056afcc973d5fcbe11421b6d5ddc57b81786765302030d7fc893bf729", "scriptKind": 3, }) ); @@ -1012,7 +1012,7 @@ mod tests { #[serde(rename_all = "camelCase")] struct LoadResponse { data: String, - hash: Option, + version: Option, script_kind: i64, } @@ -1033,7 +1033,7 @@ mod tests { serde_json::from_value(value).expect("failed to deserialize"); let expected = get_asset("lib.dom.d.ts").unwrap(); assert_eq!(actual.data, expected); - assert!(actual.hash.is_some()); + assert!(actual.version.is_some()); assert_eq!(actual.script_kind, 3); } @@ -1052,7 +1052,7 @@ mod tests { actual, json!({ "data": "some content", - "hash": null, + "version": null, "scriptKind": 0, }) ); @@ -1070,7 +1070,7 @@ mod tests { actual, json!({ "data": null, - "hash": null, + "version": null, "scriptKind": 0, }) ) diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 68f4f37f62..6d47b6ac70 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -72,6 +72,8 @@ delete Object.prototype.__proto__; } } + // In the case of the LSP, this is initialized with the assets + // when snapshotting and never added to or removed after that. /** @type {Map} */ const sourceFileCache = new Map(); @@ -186,62 +188,6 @@ delete Object.prototype.__proto__; target: ts.ScriptTarget.ESNext, }; - class ScriptSnapshot { - /** @type {string} */ - specifier; - /** @type {string} */ - version; - /** - * @param {string} specifier - * @param {string} version - */ - constructor(specifier, version) { - this.specifier = specifier; - this.version = version; - } - /** - * @param {number} start - * @param {number} end - * @returns {string} - */ - getText(start, end) { - const { specifier, version } = this; - debug( - `snapshot.getText(${start}, ${end}) specifier: ${specifier} version: ${version}`, - ); - return core.opSync("op_get_text", { specifier, version, start, end }); - } - /** - * @returns {number} - */ - getLength() { - const { specifier, version } = this; - debug(`snapshot.getLength() specifier: ${specifier} version: ${version}`); - return core.opSync("op_get_length", { specifier, version }); - } - /** - * @param {ScriptSnapshot} oldSnapshot - * @returns {ts.TextChangeRange | undefined} - */ - getChangeRange(oldSnapshot) { - const { specifier, version } = this; - const { version: oldVersion } = oldSnapshot; - const oldLength = oldSnapshot.getLength(); - debug( - `snapshot.getLength() specifier: ${specifier} oldVersion: ${oldVersion} version: ${version}`, - ); - return core.opSync( - "op_get_change_range", - { specifier, oldLength, oldVersion, version }, - ); - } - dispose() { - const { specifier, version } = this; - debug(`snapshot.dispose() specifier: ${specifier} version: ${version}`); - core.opSync("op_dispose", { specifier, version }); - } - } - /** Error thrown on cancellation. */ class OperationCanceledError extends Error { } @@ -310,16 +256,17 @@ delete Object.prototype.__proto__; ts.ScriptTarget[languageVersion] })`, ); + + // Needs the original specifier + specifier = normalizedToOriginalMap.get(specifier) ?? specifier; + let sourceFile = sourceFileCache.get(specifier); if (sourceFile) { return sourceFile; } - // Needs the original specifier - specifier = normalizedToOriginalMap.get(specifier) ?? specifier; - - /** @type {{ data: string; hash?: string; scriptKind: ts.ScriptKind }} */ - const { data, hash, scriptKind } = core.opSync( + /** @type {{ data: string; scriptKind: ts.ScriptKind }} */ + const { data, scriptKind, version } = core.opSync( "op_load", { specifier }, ); @@ -335,8 +282,9 @@ delete Object.prototype.__proto__; scriptKind, ); sourceFile.moduleName = specifier; - sourceFile.version = hash; + sourceFile.version = version; sourceFileCache.set(specifier, sourceFile); + scriptVersionCache.set(specifier, version); return sourceFile; }, getDefaultLibFileName() { @@ -445,11 +393,17 @@ delete Object.prototype.__proto__; }, }; } - const version = host.getScriptVersion(specifier); - if (version != null) { - return new ScriptSnapshot(specifier, version); + + const fileInfo = core.opSync( + "op_load", + { specifier }, + ); + if (fileInfo) { + scriptVersionCache.set(specifier, fileInfo.version); + return ts.ScriptSnapshot.fromString(fileInfo.data); + } else { + return undefined; } - return undefined; }, };