mod check;
mod handlers_code_fixes;
mod handlers_completions;
mod handlers_completions_snippets;
mod handlers_diagnostics;
mod handlers_editing;
mod handlers_files;
mod handlers_info;
mod handlers_legacy;
mod handlers_quickinfo;
mod handlers_structure;
use anyhow::{Context, Result};
use clap::Parser;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::io::{BufRead, BufReader, Read as IoRead, Write};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{debug, info};
use tsz::binder::BinderState;
use tsz::lib_loader::LibFile;
use tsz::lsp::position::Position;
use tsz::parser::ParserState;
use tsz::parser::base::NodeIndex;
use tsz::parser::node::NodeArena;
const TS1490_FILE_APPEARS_TO_BE_BINARY: i32 = 1490;
fn content_appears_binary(content: &str) -> bool {
if content.is_empty() {
return false;
}
let max_bytes = content.len().min(512);
let check_slice = if max_bytes >= content.len() {
content
} else {
let mut boundary = max_bytes;
while !content.is_char_boundary(boundary) && boundary > 0 {
boundary -= 1;
}
&content[..boundary]
};
let replacement_count = check_slice.matches('\u{FFFD}').count();
if replacement_count >= 3 {
return true;
}
let null_count = check_slice.chars().filter(|&c| c == '\0').count();
if null_count >= 4 {
return true;
}
let control_count = check_slice
.chars()
.filter(|&ch| {
let code = ch as u32;
code <= 0x1f
&& code != 0x09
&& code != 0x0A
&& code != 0x0D
&& code != 0x0B
&& code != 0x0C
})
.count();
if control_count >= 4 {
return true;
}
false
}
#[derive(Parser, Debug)]
#[command(
name = "tsz-server",
version,
about = "TypeScript language server - tsserver compatible"
)]
struct ServerArgs {
#[arg(long = "syntaxOnly", alias = "syntax-only")]
syntax_only: bool,
#[arg(
long = "useSingleInferredProject",
alias = "use-single-inferred-project"
)]
use_single_inferred_project: bool,
#[arg(
long = "useInferredProjectPerProjectRoot",
alias = "use-inferred-project-per-project-root"
)]
use_inferred_project_per_project_root: bool,
#[arg(
long = "suppressDiagnosticEvents",
alias = "suppress-diagnostic-events"
)]
suppress_diagnostic_events: bool,
#[arg(
long = "noGetErrOnBackgroundUpdate",
alias = "no-get-err-on-background-update"
)]
no_get_err_on_background_update: bool,
#[arg(long = "allowLocalPluginLoads", alias = "allow-local-plugin-loads")]
allow_local_plugin_loads: bool,
#[arg(long = "canUseWatchEvents", alias = "can-use-watch-events")]
can_use_watch_events: bool,
#[arg(
long = "disableAutomaticTypingAcquisition",
alias = "disable-automatic-typing-acquisition"
)]
disable_automatic_typing_acquisition: bool,
#[arg(long = "enableTelemetry", alias = "enable-telemetry")]
enable_telemetry: bool,
#[arg(
long = "validateDefaultNpmLocation",
alias = "validate-default-npm-location"
)]
validate_default_npm_location: bool,
#[arg(long = "cancellationPipeName", alias = "cancellation-pipe-name")]
cancellation_pipe_name: Option<String>,
#[arg(long = "serverMode", alias = "server-mode")]
server_mode: Option<String>,
#[arg(long = "eventPort", alias = "event-port")]
event_port: Option<u16>,
#[arg(long)]
locale: Option<String>,
#[arg(
long = "globalPlugins",
alias = "global-plugins",
value_delimiter = ','
)]
global_plugins: Option<Vec<String>>,
#[arg(
long = "pluginProbeLocations",
alias = "plugin-probe-locations",
value_delimiter = ','
)]
plugin_probe_locations: Option<Vec<String>>,
#[arg(long = "logVerbosity", alias = "log-verbosity")]
log_verbosity: Option<String>,
#[arg(long = "logFile", alias = "log-file")]
log_file: Option<PathBuf>,
#[arg(long = "traceDirectory", alias = "trace-directory")]
trace_directory: Option<PathBuf>,
#[arg(long = "npmLocation", alias = "npm-location")]
npm_location: Option<PathBuf>,
#[arg(
long = "enableProjectWideIntelliSenseOnWeb",
alias = "enable-project-wide-intellisense-on-web"
)]
enable_project_wide_intellisense_on_web: bool,
#[arg(long = "useNodeIpc", alias = "use-node-ipc")]
use_node_ipc: bool,
#[arg(long, default_value = "tsserver")]
protocol: Protocol,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
enum Protocol {
Tsserver,
Legacy,
}
#[derive(Debug, Deserialize)]
pub(crate) struct TsServerRequest {
pub(crate) seq: u64,
#[serde(rename = "type")]
pub(crate) _msg_type: String,
pub(crate) command: String,
#[serde(default)]
pub(crate) arguments: serde_json::Value,
}
#[derive(Debug, Serialize)]
pub(crate) struct TsServerResponse {
pub(crate) seq: u64,
#[serde(rename = "type")]
pub(crate) msg_type: String,
pub(crate) command: String,
pub(crate) request_seq: u64,
pub(crate) success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) body: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
enum LegacyRequest {
Check {
id: u64,
files: Box<FxHashMap<String, String>>,
#[serde(default)]
options: Box<CheckOptions>,
},
Status { id: u64 },
Recycle { id: u64 },
Shutdown { id: u64 },
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CheckOptions {
#[serde(default)]
strict: bool,
#[serde(default)]
strict_null_checks: Option<bool>,
#[serde(default)]
strict_function_types: Option<bool>,
#[serde(default)]
strict_bind_call_apply: Option<bool>,
#[serde(default)]
strict_property_initialization: Option<bool>,
#[serde(default)]
no_implicit_any: Option<bool>,
#[serde(default)]
no_implicit_this: Option<bool>,
#[serde(default)]
no_implicit_returns: bool,
#[serde(default)]
use_unknown_in_catch_variables: Option<bool>,
#[serde(default)]
always_strict: Option<bool>,
#[serde(default)]
no_unused_locals: bool,
#[serde(default)]
no_unused_parameters: bool,
#[serde(default)]
exact_optional_property_types: bool,
#[serde(default)]
no_unchecked_indexed_access: bool,
#[serde(default)]
allow_unreachable_code: Option<bool>,
#[serde(default)]
no_property_access_from_index_signature: bool,
#[serde(default)]
es_module_interop: bool,
#[serde(default)]
allow_synthetic_default_imports: Option<bool>,
#[serde(default)]
isolated_modules: bool,
#[serde(default)]
no_lib: bool,
#[serde(default)]
lib: Option<Vec<String>>,
#[serde(default)]
target: Option<String>,
#[serde(default)]
module: Option<String>,
#[serde(default)]
experimental_decorators: bool,
#[serde(default)]
no_resolve: bool,
#[serde(default)]
check_js: bool,
#[serde(default)]
resolve_json_module: bool,
#[serde(default)]
no_unchecked_side_effect_imports: bool,
#[serde(default)]
no_implicit_override: bool,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
enum LegacyResponse {
Check(CheckResponse),
Status(StatusResponse),
Ok(OkResponse),
Error(ErrorResponse),
}
#[derive(Debug, Serialize)]
struct CheckResponse {
id: u64,
codes: Vec<i32>,
elapsed_ms: u64,
}
#[derive(Debug, Serialize)]
struct StatusResponse {
id: u64,
memory_mb: u64,
checks_completed: u64,
cached_libs: usize,
}
#[derive(Debug, Serialize)]
struct OkResponse {
id: u64,
ok: bool,
}
#[derive(Debug, Serialize)]
struct ErrorResponse {
id: u64,
error: String,
}
pub(crate) struct LogConfig {
pub(crate) level: LogLevel,
pub(crate) file: Option<PathBuf>,
pub(crate) trace_to_console: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LogLevel {
Off,
Terse,
Normal,
RequestTime,
Verbose,
}
impl LogConfig {
fn from_env_and_args(args: &ServerArgs) -> Self {
let mut config = Self {
level: LogLevel::Off,
file: None,
trace_to_console: false,
};
if let Ok(tss_log) = std::env::var("TSS_LOG") {
let parts: Vec<&str> = tss_log.split_whitespace().collect();
let mut i = 0;
while i < parts.len() {
match parts[i] {
"-level" if i + 1 < parts.len() => {
config.level = match parts[i + 1] {
"terse" => LogLevel::Terse,
"normal" => LogLevel::Normal,
"requestTime" => LogLevel::RequestTime,
"verbose" => LogLevel::Verbose,
_ => LogLevel::Off,
};
i += 2;
}
"-file" if i + 1 < parts.len() => {
config.file = Some(PathBuf::from(parts[i + 1]));
i += 2;
}
"-traceToConsole" if i + 1 < parts.len() => {
config.trace_to_console = parts[i + 1] == "true";
i += 2;
}
_ => {
i += 1;
}
}
}
}
if let Some(ref verbosity) = args.log_verbosity {
config.level = match verbosity.as_str() {
"terse" => LogLevel::Terse,
"normal" => LogLevel::Normal,
"requestTime" => LogLevel::RequestTime,
"verbose" => LogLevel::Verbose,
_ => LogLevel::Off,
};
}
if let Some(ref log_file) = args.log_file {
config.file = Some(log_file.clone());
}
config
}
}
pub(crate) struct Server {
pub(crate) lib_dir: PathBuf,
pub(crate) tests_lib_dir: PathBuf,
pub(crate) lib_cache: FxHashMap<String, (Arc<LibFile>, Vec<String>)>,
pub(crate) unified_lib_cache: Option<(Vec<String>, Arc<LibFile>)>,
pub(crate) checks_completed: u64,
pub(crate) response_seq: u64,
pub(crate) open_files: FxHashMap<String, String>,
pub(crate) external_project_files: FxHashMap<String, Vec<String>>,
pub(crate) completion_import_module_specifier_ending: Option<String>,
pub(crate) import_module_specifier_preference: Option<String>,
pub(crate) organize_imports_type_order: Option<String>,
pub(crate) organize_imports_ignore_case: bool,
pub(crate) auto_import_file_exclude_patterns: Vec<String>,
pub(crate) auto_import_specifier_exclude_regexes: Vec<String>,
pub(crate) include_completions_with_class_member_snippets: bool,
pub(crate) allow_importing_ts_extensions: bool,
pub(crate) auto_imports_allowed_for_inferred_projects: bool,
pub(crate) inferred_module_is_none_for_projects: bool,
pub(crate) _server_mode: ServerMode,
pub(crate) _log_config: LogConfig,
pub(crate) enable_telemetry: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ServerMode {
Semantic,
PartialSemantic,
Syntactic,
}
impl Server {
fn new(args: &ServerArgs) -> Result<Self> {
let lib_dir = Self::find_lib_dir()?;
let tests_lib_dir = PathBuf::from("TypeScript/tests/lib");
info!("Using lib directory: {}", lib_dir.display());
let server_mode = if args.syntax_only {
ServerMode::Syntactic
} else {
match args.server_mode.as_deref() {
Some("partialSemantic") => ServerMode::PartialSemantic,
Some("syntactic") => ServerMode::Syntactic,
_ => ServerMode::Semantic,
}
};
let log_config = LogConfig::from_env_and_args(args);
if let Ok(port) = std::env::var("TSS_DEBUG") {
debug!("TSS_DEBUG detected: port {}", port);
}
if let Ok(port) = std::env::var("TSS_DEBUG_BRK") {
debug!("TSS_DEBUG_BRK detected: port {} (break on startup)", port);
}
if log_config.level != LogLevel::Off {
if let Some(ref file) = log_config.file {
info!("Log file: {}", file.display());
}
info!("Log level: {:?}", log_config.level);
}
Ok(Self {
lib_dir,
tests_lib_dir,
lib_cache: FxHashMap::default(),
unified_lib_cache: None,
checks_completed: 0,
response_seq: 0,
open_files: FxHashMap::default(),
external_project_files: FxHashMap::default(),
completion_import_module_specifier_ending: None,
import_module_specifier_preference: None,
organize_imports_type_order: None,
organize_imports_ignore_case: true,
auto_import_file_exclude_patterns: Vec::new(),
auto_import_specifier_exclude_regexes: Vec::new(),
include_completions_with_class_member_snippets: false,
allow_importing_ts_extensions: false,
auto_imports_allowed_for_inferred_projects: true,
inferred_module_is_none_for_projects: false,
_server_mode: server_mode,
_log_config: log_config,
enable_telemetry: args.enable_telemetry,
})
}
const fn next_seq(&mut self) -> u64 {
self.response_seq += 1;
self.response_seq
}
fn parse_and_bind_file(
&self,
file_path: &str,
) -> Option<(NodeArena, BinderState, NodeIndex, String)> {
let content = self
.open_files
.get(file_path)
.cloned()
.or_else(|| std::fs::read_to_string(file_path).ok())?;
let mut parser = ParserState::new(file_path.to_string(), content.clone());
let root = parser.parse_source_file();
let arena = parser.into_arena();
let mut binder = BinderState::new();
binder.bind_source_file(&arena, root);
Some((arena, binder, root, content))
}
fn extract_file_position(args: &serde_json::Value) -> Option<(String, u32, u32)> {
let file = args.get("file")?.as_str()?.to_string();
let line = args.get("line")?.as_u64()? as u32;
let offset = args.get("offset")?.as_u64()? as u32;
Some((file, line, offset))
}
fn add_project_config_files(
files: &mut rustc_hash::FxHashMap<String, String>,
file_path: &str,
) {
let mut current = std::path::Path::new(file_path).parent();
while let Some(dir) = current {
for config_name in ["package.json", "tsconfig.json", "jsconfig.json"] {
let config_path = dir.join(config_name);
let key = config_path.to_string_lossy().to_string();
if files.contains_key(&key) {
continue;
}
if let Ok(content) = std::fs::read_to_string(&config_path) {
files.insert(key, content);
}
}
current = dir.parent();
}
}
pub(crate) const fn tsserver_to_lsp_position(line: u32, offset: u32) -> Position {
Position::new(line.saturating_sub(1), offset.saturating_sub(1))
}
fn lsp_to_tsserver_position(pos: Position) -> serde_json::Value {
serde_json::json!({
"line": pos.line + 1,
"offset": pos.character + 1
})
}
fn definition_info_to_json(
info: &tsz::lsp::definition::DefinitionInfo,
file: &str,
) -> serde_json::Value {
let out_file = if info.location.file_path.is_empty() {
file.to_string()
} else {
info.location.file_path.clone()
};
let mut result = serde_json::json!({
"file": out_file,
"start": Self::lsp_to_tsserver_position(info.location.range.start),
"end": Self::lsp_to_tsserver_position(info.location.range.end),
"kind": info.kind,
"name": info.name,
"containerName": info.container_name,
"containerKind": info.container_kind,
"isLocal": info.is_local,
"isAmbient": info.is_ambient,
"unverified": false,
});
if info.kind == "alias" {
result["failedAliasResolution"] = serde_json::json!(false);
}
if let Some(ref ctx) = info.context_span {
result["contextStart"] = Self::lsp_to_tsserver_position(ctx.start);
result["contextEnd"] = Self::lsp_to_tsserver_position(ctx.end);
}
result
}
fn find_lib_dir() -> Result<PathBuf> {
let cwd = std::env::current_dir().context("Failed to get CWD")?;
if let Ok(dir) = std::env::var("TSZ_LIB_DIR") {
let path = PathBuf::from(&dir);
let path = if path.is_absolute() {
path
} else {
cwd.join(&path)
};
if path.exists() {
return Ok(path);
}
}
let lib_path = cwd.join("TypeScript/src/lib");
if lib_path.exists() {
return Ok(lib_path);
}
let mut current = cwd.clone();
for _ in 0..10 {
let candidate = current.join("TypeScript/src/lib");
if candidate.exists() {
return Ok(candidate);
}
current = match current.parent() {
Some(p) => p.to_path_buf(),
None => break,
};
}
anyhow::bail!(
"TypeScript lib directory not found. \
CWD: {}. \
Checked: TypeScript/src/lib (relative to CWD), TSZ_LIB_DIR env var, \
and walked up 10 directories looking for TypeScript/src/lib. \
Run from project root or set TSZ_LIB_DIR to an absolute path.",
cwd.display()
)
}
fn handle_tsserver_request(&mut self, request: TsServerRequest) -> TsServerResponse {
let seq = self.next_seq();
match request.command.as_str() {
"open" => self.handle_open(seq, &request),
"close" => self.handle_close(seq, &request),
"change" => self.handle_change(seq, &request),
"configure" => self.handle_configure(seq, &request),
"quickinfo" => self.handle_quickinfo(seq, &request),
"definition"
| "typeDefinition"
| "definition-full"
| "typeDefinition-full"
| "findSourceDefinition" => self.handle_definition(seq, &request),
"definitionAndBoundSpan" | "definitionAndBoundSpan-full" => {
self.handle_definition_and_bound_span(seq, &request)
}
"references" => self.handle_references(seq, &request),
"references-full" => self.handle_references_full(seq, &request),
"completions" | "completionInfo" => self.handle_completions(seq, &request),
"completionEntryDetails" | "completionEntryDetails-full" => {
self.handle_completion_details(seq, &request)
}
"signatureHelp" => self.handle_signature_help(seq, &request),
"semanticDiagnosticsSync" => self.handle_semantic_diagnostics_sync(seq, &request),
"syntacticDiagnosticsSync" => self.handle_syntactic_diagnostics_sync(seq, &request),
"suggestionDiagnosticsSync" => self.handle_suggestion_diagnostics_sync(seq, &request),
"geterr" => self.handle_geterr(seq, &request),
"geterrForProject" => self.handle_geterr_for_project(seq, &request),
"navtree" => self.handle_navtree(seq, &request),
"navbar" => self.handle_navbar(seq, &request),
"navto" | "navTo" | "navto-full" | "navTo-full" => self.handle_navto(seq, &request),
"documentHighlights" => self.handle_document_highlights(seq, &request),
"rename" | "rename-full" => self.handle_rename(seq, &request),
"getCodeFixes" => self.handle_get_code_fixes(seq, &request),
"getCombinedCodeFix" => self.handle_get_combined_code_fix(seq, &request),
"applyCodeActionCommand" => self.handle_apply_code_action_command(seq, &request),
"getSupportedCodeFixes" => self.handle_get_supported_code_fixes(seq, &request),
"getApplicableRefactors" => self.handle_get_applicable_refactors(seq, &request),
"getEditsForRefactor" => self.handle_get_edits_for_refactor(seq, &request),
"organizeImports" => self.handle_organize_imports(seq, &request),
"getEditsForFileRename" => self.handle_get_edits_for_file_rename(seq, &request),
"format" => self.handle_format(seq, &request),
"formatonkey" => self.handle_format_on_key(seq, &request),
"projectInfo" => self.handle_project_info(seq, &request),
"compilerOptionsForInferredProjects" => {
self.handle_compiler_options_for_inferred(seq, &request)
}
"openExternalProject" | "openExternalProjects" | "closeExternalProject" => {
self.handle_external_project(seq, &request)
}
"updateOpen" => self.handle_update_open(seq, &request),
"encodedSemanticClassifications-full" => {
self.handle_encoded_semantic_classifications_full(seq, &request)
}
"inlayHints" | "provideInlayHints" => self.handle_inlay_hints(seq, &request),
"selectionRange" => self.handle_selection_range(seq, &request),
"linkedEditingRange" => self.handle_linked_editing_range(seq, &request),
"prepareCallHierarchy" => self.handle_prepare_call_hierarchy(seq, &request),
"provideCallHierarchyIncomingCalls" | "provideCallHierarchyOutgoingCalls" => {
self.handle_call_hierarchy(seq, &request)
}
"mapCode" => self.handle_map_code(seq, &request),
"fileReferences" => self.handle_file_references(seq, &request),
"implementation" | "implementation-full" => self.handle_implementation(seq, &request),
"getOutliningSpans" => self.handle_outlining_spans(seq, &request),
"brace" => self.handle_brace(seq, &request),
"tszPerformance" | "performance" => self.handle_tsz_performance(seq, &request),
"emitOutput" | "emit-output" => self.stub_response(
seq,
&request,
Some(serde_json::json!({"outputFiles": [], "emitSkipped": true})),
),
"getMoveToRefactoringFileSuggestions" => self.stub_response(
seq,
&request,
Some(serde_json::json!({"newFileName": "", "files": []})),
),
"preparePasteEdits" => {
self.stub_response(seq, &request, Some(serde_json::json!(false)))
}
"getPasteEdits" => self.stub_response(
seq,
&request,
Some(serde_json::json!({"edits": [], "fixId": ""})),
),
"configurePlugin" => self.stub_response(seq, &request, None),
"breakpointStatement" => self.handle_breakpoint_statement(seq, &request),
"jsxClosingTag" => self.handle_jsx_closing_tag(seq, &request),
"braceCompletion" => self.handle_brace_completion(seq, &request),
"getSpanOfEnclosingComment" => self.handle_span_of_enclosing_comment(seq, &request),
"todoComments" => self.handle_todo_comments(seq, &request),
"docCommentTemplate" => self.handle_doc_comment_template(seq, &request),
"indentation" => self.handle_indentation(seq, &request),
"toggleLineComment" | "toggleLineComment-full" => {
self.handle_toggle_line_comment(seq, &request)
}
"toggleMultilineComment" | "toggleMultilineComment-full" => {
self.handle_toggle_multiline_comment(seq, &request)
}
"commentSelection" | "commentSelection-full" => {
self.handle_comment_selection(seq, &request)
}
"uncommentSelection" | "uncommentSelection-full" => {
self.handle_uncomment_selection(seq, &request)
}
"getSmartSelectionRange" => self.handle_smart_selection_range(seq, &request),
"getSyntacticClassifications" => self.handle_syntactic_classifications(seq, &request),
"getSemanticClassifications" => self.handle_semantic_classifications(seq, &request),
"getCompilerOptionsDiagnostics" => {
self.handle_compiler_options_diagnostics(seq, &request)
}
"exit" => TsServerResponse {
seq,
msg_type: "response".to_string(),
command: request.command.clone(),
request_seq: request.seq,
success: true,
message: None,
body: None,
},
_ => TsServerResponse {
seq,
msg_type: "response".to_string(),
command: request.command.clone(),
request_seq: request.seq,
success: false,
message: Some(format!("Unrecognized command: {}", request.command)),
body: None,
},
}
}
pub(crate) fn stub_response(
&self,
seq: u64,
request: &TsServerRequest,
body: Option<serde_json::Value>,
) -> TsServerResponse {
TsServerResponse {
seq,
msg_type: "response".to_string(),
command: request.command.clone(),
request_seq: request.seq,
success: true,
message: None,
body,
}
}
}
fn build_code_map(bytes: &[u8]) -> Vec<bool> {
let len = bytes.len();
let mut map = vec![true; len];
let mut i = 0;
while i < len {
match bytes[i] {
b'/' if i + 1 < len => {
if bytes[i + 1] == b'/' {
map[i] = false;
map[i + 1] = false;
i += 2;
while i < len && bytes[i] != b'\n' {
map[i] = false;
i += 1;
}
} else if bytes[i + 1] == b'*' {
map[i] = false;
map[i + 1] = false;
i += 2;
while i < len {
if bytes[i] == b'*' && i + 1 < len && bytes[i + 1] == b'/' {
map[i] = false;
map[i + 1] = false;
i += 2;
break;
}
map[i] = false;
i += 1;
}
} else {
i += 1;
}
}
b'"' | b'\'' => {
let quote = bytes[i];
map[i] = false;
i += 1;
while i < len {
if bytes[i] == b'\\' {
map[i] = false;
i += 1;
if i < len {
map[i] = false;
i += 1;
}
} else if bytes[i] == quote {
map[i] = false;
i += 1;
break;
} else if bytes[i] == b'\n' {
break;
} else {
map[i] = false;
i += 1;
}
}
}
b'`' => {
map[i] = false;
i += 1;
let mut depth = 0u32;
while i < len {
if bytes[i] == b'\\' {
map[i] = false;
i += 1;
if i < len {
map[i] = false;
i += 1;
}
} else if bytes[i] == b'$' && i + 1 < len && bytes[i + 1] == b'{' {
depth += 1;
i += 2;
} else if bytes[i] == b'{' && depth > 0 {
depth += 1;
i += 1;
} else if bytes[i] == b'}' && depth > 0 {
depth -= 1;
i += 1;
} else if bytes[i] == b'`' && depth == 0 {
map[i] = false;
i += 1;
break;
} else {
if depth == 0 {
map[i] = false;
}
i += 1;
}
}
}
_ => {
i += 1;
}
}
}
map
}
fn scan_forward(
bytes: &[u8],
code_map: &[bool],
start: usize,
open: u8,
close: u8,
) -> Option<usize> {
let mut depth = 1i32;
let mut i = start + 1;
while i < bytes.len() {
if code_map[i] {
if bytes[i] == open {
depth += 1;
} else if bytes[i] == close {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
}
i += 1;
}
None
}
fn scan_backward(
bytes: &[u8],
code_map: &[bool],
start: usize,
close: u8,
open: u8,
) -> Option<usize> {
let mut depth = 1i32;
let mut i = start;
while i > 0 {
i -= 1;
if code_map[i] {
if bytes[i] == close {
depth += 1;
} else if bytes[i] == open {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
}
}
None
}
fn find_angle_bracket_match(arena: &NodeArena, source: &str, pos: usize) -> Option<usize> {
let bytes = source.as_bytes();
let mut pairs: Vec<(usize, usize)> = Vec::new();
let check_list_nodes = |list: &Option<tsz::parser::base::NodeList>| -> Option<(usize, usize)> {
let list = list.as_ref()?;
if list.nodes.is_empty() {
return None;
}
let first = arena.nodes.get(list.nodes.first()?.0 as usize)?;
let last = arena.nodes.get(list.nodes.last()?.0 as usize)?;
let open_pos = (first.pos as usize).checked_sub(1)?;
if bytes.get(open_pos) != Some(&b'<') {
return None;
}
let close_candidate1 = last.end as usize;
if close_candidate1 > 0 && bytes.get(close_candidate1 - 1) == Some(&b'>') {
return Some((open_pos, close_candidate1 - 1));
}
if bytes.get(close_candidate1) == Some(&b'>') {
return Some((open_pos, close_candidate1));
}
None
};
for f in &arena.functions {
if let Some(pair) = check_list_nodes(&f.type_parameters) {
pairs.push(pair);
}
}
for c in &arena.classes {
if let Some(pair) = check_list_nodes(&c.type_parameters) {
pairs.push(pair);
}
}
for iface in &arena.interfaces {
if let Some(pair) = check_list_nodes(&iface.type_parameters) {
pairs.push(pair);
}
}
for t in &arena.type_aliases {
if let Some(pair) = check_list_nodes(&t.type_parameters) {
pairs.push(pair);
}
}
for c in &arena.call_exprs {
if let Some(pair) = check_list_nodes(&c.type_arguments) {
pairs.push(pair);
}
}
for t in &arena.type_refs {
if let Some(pair) = check_list_nodes(&t.type_arguments) {
pairs.push(pair);
}
}
for s in &arena.signatures {
if let Some(pair) = check_list_nodes(&s.type_parameters) {
pairs.push(pair);
}
}
for m in &arena.method_decls {
if let Some(pair) = check_list_nodes(&m.type_parameters) {
pairs.push(pair);
}
}
for c in &arena.constructors {
if let Some(pair) = check_list_nodes(&c.type_parameters) {
pairs.push(pair);
}
}
for ft in &arena.function_types {
if let Some(pair) = check_list_nodes(&ft.type_parameters) {
pairs.push(pair);
}
}
for e in &arena.expr_with_type_args {
if let Some(pair) = check_list_nodes(&e.type_arguments) {
pairs.push(pair);
}
}
for node in &arena.nodes {
if node.kind == tsz::parser::syntax_kind_ext::TYPE_ASSERTION
&& let Some(ta) = arena.type_assertions.get(node.data_index as usize)
{
let open_pos = node.pos as usize;
if bytes.get(open_pos) != Some(&b'<') {
continue;
}
if let Some(type_node) = arena.nodes.get(ta.type_node.0 as usize) {
let end = type_node.end as usize;
if end > 0 && bytes.get(end - 1) == Some(&b'>') {
pairs.push((open_pos, end - 1));
} else if bytes.get(end) == Some(&b'>') {
pairs.push((open_pos, end));
}
}
}
}
for (open, close) in pairs {
if pos == open {
return Some(close);
} else if pos == close {
return Some(open);
}
}
None
}
fn read_content_length_message(reader: &mut BufReader<std::io::Stdin>) -> Result<Option<String>> {
let mut header_line = String::new();
let bytes_read = reader.read_line(&mut header_line)?;
if bytes_read == 0 {
return Ok(None); }
let header = header_line.trim();
if header.is_empty() {
return read_content_length_message(reader);
}
let content_length = if let Some(len_str) = header.strip_prefix("Content-Length:") {
len_str
.trim()
.parse::<usize>()
.with_context(|| format!("invalid Content-Length: {}", len_str.trim()))?
} else {
return Ok(Some(header.to_string()));
};
let mut blank_line = String::new();
reader.read_line(&mut blank_line)?;
let mut body = vec![0u8; content_length];
reader.read_exact(&mut body)?;
String::from_utf8(body)
.map(Some)
.context("invalid UTF-8 in message body")
}
fn write_content_length_message(stdout: &mut std::io::Stdout, message: &str) -> Result<()> {
write!(
stdout,
"Content-Length: {}\r\n\r\n{}",
message.len(),
message
)?;
stdout.flush()?;
Ok(())
}
fn main() -> Result<()> {
tsz_cli::tracing_config::init_tracing();
let args = ServerArgs::parse();
let mut server = Server::new(&args).context("failed to initialize server")?;
info!("tsz-server ready (protocol: {:?})", args.protocol);
match args.protocol {
Protocol::Tsserver => run_tsserver_protocol(&mut server)?,
Protocol::Legacy => run_legacy_protocol(&mut server)?,
}
Ok(())
}
fn run_tsserver_protocol(server: &mut Server) -> Result<()> {
let mut stdin = BufReader::new(std::io::stdin());
let mut stdout = std::io::stdout();
loop {
let message = match read_content_length_message(&mut stdin)? {
Some(msg) => msg,
None => break, };
if message.trim().is_empty() {
continue;
}
let request: TsServerRequest = match serde_json::from_str(&message) {
Ok(req) => req,
Err(e) => {
let error_response = TsServerResponse {
seq: server.next_seq(),
msg_type: "response".to_string(),
command: "unknown".to_string(),
request_seq: 0,
success: false,
message: Some(format!("invalid request: {e}")),
body: None,
};
let json = serde_json::to_string(&error_response)?;
write_content_length_message(&mut stdout, &json)?;
continue;
}
};
let is_exit = request.command == "exit";
let response = server.handle_tsserver_request(request);
let json = serde_json::to_string(&response)?;
write_content_length_message(&mut stdout, &json)?;
if is_exit {
break;
}
}
Ok(())
}
fn run_legacy_protocol(server: &mut Server) -> Result<()> {
let stdin = BufReader::new(std::io::stdin());
let mut stdout = std::io::stdout();
for line in stdin.lines() {
let line = line.context("failed to read from stdin")?;
if line.trim().is_empty() {
continue;
}
let request: LegacyRequest = match serde_json::from_str(&line) {
Ok(req) => req,
Err(e) => {
let error_response = LegacyResponse::Error(ErrorResponse {
id: 0,
error: format!("invalid request: {e}"),
});
writeln!(stdout, "{}", serde_json::to_string(&error_response)?)?;
stdout.flush()?;
continue;
}
};
let is_shutdown = matches!(request, LegacyRequest::Shutdown { .. });
let response = server.handle_legacy_request(request);
writeln!(stdout, "{}", serde_json::to_string(&response)?)?;
stdout.flush()?;
if is_shutdown {
break;
}
}
Ok(())
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;