дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
Project & files - Project view modes (Project Files / Open Files / All Changed Files / Scratches); External Libraries for Python, JDK (src.zip), Node, Cargo, Go, Maven/Gradle jars; read-only jar/zip browsing - ⌘N New popup with type search and more file kinds; New Project, Rename Project, Reload All from Disk, Export/Import Settings, Power Save Mode - File type associations, extra content roots and exclude patterns (indexed and watched without reopening the project) Editor & navigation - Find bar: search scope (comments/strings), Preserve Case, Exclude, multiline, history; Find in Files: Module scope, custom pattern scopes, multiline - Search Everywhere filters, Assign Shortcut, settings results; named bookmark lists, file/folder bookmarks; Structure filters and Show Inherited; Recent Locations timestamps and breadcrumbs; Code Vision author - Completion options, editor tab placement/layout, inline completion (LSP 3.18) - Spell checking (Typo inspection, ide-spell) and Inspections settings with custom profiles; Code Style schemes, formatter markers, Do not format Run, VCS, services, database - Typed run configurations (Shell, Program, npm, Gradle, Maven, Docker, Compound), Modify options, Before launch, env dialog, Current File, Emulate terminal (PTY) - Git log graph/view options, Go to Hash/Branch/Tag, date filter keeps the graph; Pull/Merge/Rebase dialogs, Manage Remotes, Worktrees - Generic Services host (Docker connections/registries, run sessions); TODO scope/changes tabs with changelists; Coverage report import - Data sources: wire-compatible vendors, URL import, SSL certs, Options, Schemas and Advanced tabs, color and comment Workbench - Run Anything, Customize Main Toolbar, named tool window layouts, menu completeness, Code Cleanup, macros, "Don't Show Again", editor banners - Chinese translations for the new UI Claude-Session: https://claude.ai/code/session_01SaPPsVPuk6ZTNsoUCNCbMv
160 рядки
5.5 KiB
Rust
160 рядки
5.5 KiB
Rust
//! `.env` files ("Paths to .env files" / EnvFile): `KEY=value` lines, `export`
|
|
//! prefixes, `#` comments, single- and double-quoted values (escapes in double
|
|
//! quotes) and `${VAR}` references to earlier variables or the base environment.
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
/// Parses `text`; `lookup` resolves `${VAR}` / `$VAR` not defined earlier in the file.
|
|
pub fn parse(text: &str, lookup: &dyn Fn(&str) -> Option<String>) -> Vec<(String, String)> {
|
|
let mut out: Vec<(String, String)> = Vec::new();
|
|
let mut lines = text.lines();
|
|
while let Some(raw) = lines.next() {
|
|
let line = raw.trim_start_matches('\u{feff}').trim();
|
|
if line.is_empty() || line.starts_with('#') {
|
|
continue;
|
|
}
|
|
let line = line.strip_prefix("export ").map(str::trim_start).unwrap_or(line);
|
|
let Some((key, rest)) = line.split_once('=') else { continue };
|
|
let key = key.trim();
|
|
if key.is_empty() || key.contains(char::is_whitespace) {
|
|
continue;
|
|
}
|
|
let rest = rest.trim_start();
|
|
let value = if let Some(body) = rest.strip_prefix('\'') {
|
|
quoted(body, '\'', &mut lines).unwrap_or_else(|| rest.to_owned())
|
|
} else if let Some(body) = rest.strip_prefix('"') {
|
|
let v = quoted(body, '"', &mut lines).unwrap_or_else(|| rest.to_owned());
|
|
expand(&unescape(&v), &out, lookup)
|
|
} else {
|
|
let v = match rest.find(" #") {
|
|
Some(i) => &rest[..i],
|
|
None => rest,
|
|
};
|
|
expand(v.trim_end(), &out, lookup)
|
|
};
|
|
out.retain(|(k, _)| k != key);
|
|
out.push((key.to_owned(), value));
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The body of a quoted value up to the closing `quote`, which may be on a later line.
|
|
fn quoted<'a>(first: &str, quote: char, lines: &mut impl Iterator<Item = &'a str>) -> Option<String> {
|
|
let mut text = first.to_owned();
|
|
loop {
|
|
let mut escaped = false;
|
|
for (i, c) in text.char_indices() {
|
|
if quote == '"' && c == '\\' && !escaped {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if c == quote && !escaped {
|
|
return Some(text[..i].to_owned());
|
|
}
|
|
escaped = false;
|
|
}
|
|
text.push('\n');
|
|
text.push_str(lines.next()?);
|
|
}
|
|
}
|
|
|
|
fn unescape(s: &str) -> String {
|
|
let mut out = String::with_capacity(s.len());
|
|
let mut chars = s.chars();
|
|
while let Some(c) = chars.next() {
|
|
if c != '\\' {
|
|
out.push(c);
|
|
continue;
|
|
}
|
|
match chars.next() {
|
|
Some('n') => out.push('\n'),
|
|
Some('t') => out.push('\t'),
|
|
Some('r') => out.push('\r'),
|
|
Some(other) => out.push(other),
|
|
None => out.push('\\'),
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn expand(value: &str, earlier: &[(String, String)], lookup: &dyn Fn(&str) -> Option<String>) -> String {
|
|
let resolve = |name: &str| {
|
|
earlier
|
|
.iter()
|
|
.rev()
|
|
.find(|(k, _)| k == name)
|
|
.map(|(_, v)| v.clone())
|
|
.or_else(|| lookup(name))
|
|
.unwrap_or_default()
|
|
};
|
|
let mut out = String::with_capacity(value.len());
|
|
let bytes = value.as_bytes();
|
|
let mut i = 0;
|
|
while i < value.len() {
|
|
if bytes[i] == b'$' {
|
|
if bytes.get(i + 1) == Some(&b'{')
|
|
&& let Some(end) = value[i + 2..].find('}')
|
|
{
|
|
out.push_str(&resolve(&value[i + 2..i + 2 + end]));
|
|
i += end + 3;
|
|
continue;
|
|
}
|
|
let name_len = value[i + 1..].bytes().take_while(|b| b.is_ascii_alphanumeric() || *b == b'_').count();
|
|
if name_len > 0 {
|
|
out.push_str(&resolve(&value[i + 1..i + 1 + name_len]));
|
|
i += name_len + 1;
|
|
continue;
|
|
}
|
|
}
|
|
let c = value[i..].chars().next().unwrap_or_default();
|
|
out.push(c);
|
|
i += c.len_utf8();
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Variables of every file in order (later files win); unreadable files are an error.
|
|
pub fn load(
|
|
paths: &[std::path::PathBuf],
|
|
base: &BTreeMap<String, String>,
|
|
) -> std::io::Result<BTreeMap<String, String>> {
|
|
let mut vars: BTreeMap<String, String> = BTreeMap::new();
|
|
for path in paths {
|
|
let text = std::fs::read_to_string(path)
|
|
.map_err(|e| std::io::Error::new(e.kind(), format!("{}: {e}", path.display())))?;
|
|
let lookup = |name: &str| vars.get(name).or_else(|| base.get(name)).cloned();
|
|
let parsed = parse(&text, &lookup);
|
|
vars.extend(parsed);
|
|
}
|
|
Ok(vars)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn none(_: &str) -> Option<String> {
|
|
None
|
|
}
|
|
|
|
#[test]
|
|
fn parses_common_forms() {
|
|
let text = "# comment\nA=1\nexport B = two words # trailing\nC='lit $A'\nD=\"x\\ny $A ${B}\"\nE=\"multi\nline\"\n\nbad line\nF=\n";
|
|
let v = parse(text, &none);
|
|
let get = |k: &str| v.iter().find(|(n, _)| n == k).map(|(_, v)| v.as_str());
|
|
assert_eq!(get("A"), Some("1"));
|
|
assert_eq!(get("B"), Some("two words"));
|
|
assert_eq!(get("C"), Some("lit $A"));
|
|
assert_eq!(get("D"), Some("x\ny 1 two words"));
|
|
assert_eq!(get("E"), Some("multi\nline"));
|
|
assert_eq!(get("F"), Some(""));
|
|
assert_eq!(v.len(), 6);
|
|
}
|
|
|
|
#[test]
|
|
fn references_fall_back_to_base() {
|
|
let v = parse("P=$HOME/bin:${MISSING}x", &|n| (n == "HOME").then(|| "/h".to_owned()));
|
|
assert_eq!(v, [("P".to_owned(), "/h/bin:x".to_owned())]);
|
|
}
|
|
}
|