// Copyright (c) Microsoft Corporation. All rights reserved. #light namespace Microsoft.FSharp.Compiler.Interactive open System open System.Text open System.Collections.Generic /// System.Console.ReadKey appears to return an ANSI character (not the expected the unicode character). /// When this fix flag is true, this byte is converted to a char using the System.Console.InputEncoding. /// This is a code-around for bug://1345. /// Fixes to System.Console.ReadKey may break this code around, hence the option here. module ConsoleOptions = // Bug 4254 tracks making this fix up version specific, e.g. // let fixupRequired = typeof.Assembly.GetName().Version.Major = 2 // // Until we hear that this will be fixed in Dev10, we apply this fix always. let fixNonUnicodeSystemConsoleReadKey = ref true let readKeyFixup (c:char) = if !fixNonUnicodeSystemConsoleReadKey then // Assumes the c:char is actually a byte in the System.Console.InputEncoding. // Convert it to a Unicode char through the encoding. if 0 <= int c && int c <= 255 then let chars = System.Console.InputEncoding.GetChars [| byte c |] if chars.Length = 1 then chars.[0] // fixed up char else assert("readKeyFixHook: InputEncoding.GetChars(single-byte) returned multiple chars" = "") c // no fix up else assert("readKeyFixHook: given char is outside the 0..255 byte range" = "") c // no fix up else c type Style = Prompt | Out | Error /// Class managing the command History. type History() = let list = new List() let mutable current = 0 member x.Count = list.Count member x.Current = if current >= 0 && current < list.Count then list.[current] else String.Empty member x.Clear() = list.Clear(); current <- -1 member x.Add line = match line with | null | "" -> () | _ -> list.Add(line) member x.AddLast line = match line with | null | "" -> () | _ -> list.Add(line); current <- list.Count // Dead code // member x.First() = current <- 0; x.Current // member x.Last() = current <- list.Count - 1; x.Current; member x.Previous() = if (list.Count > 0) then current <- ((current - 1) + list.Count) % list.Count x.Current member x.Next() = if (list.Count > 0) then current <- (current + 1) % list.Count x.Current /// List of available optionsCache type Options() = inherit History() let mutable root = "" member x.Root with get() = root and set(v) = (root <- v) /// Cursor position management module Utils = let guard(f) = try f() with e -> Microsoft.FSharp.Compiler.ErrorLogger.warning(Failure(Printf.sprintf "Note: an unexpected exception in fsi.exe readline console support. Consider starting fsi.exe with the --no-readline option and report the stack trace below to the .NET or Mono implementors\n%s\n%s\n" e.Message e.StackTrace)); [] type Cursor = static member ResetTo(top,left) = Utils.guard(fun () -> Console.CursorTop <- top; Console.CursorLeft <- left) static member Move(inset, delta) = let position = Console.CursorTop * (Console.BufferWidth - inset) + (Console.CursorLeft - inset) + delta let top = position / (Console.BufferWidth - inset) let left = inset + position % (Console.BufferWidth - inset) Cursor.ResetTo(top,left) type Anchor = {top:int; left:int} static member Current(inset) = {top=Console.CursorTop;left= max inset Console.CursorLeft} member p.PlaceAt(inset, index) = //printf "p.top = %d, p.left = %d, inset = %d, index = %d\n" p.top p.left inset index; let left = inset + (( (p.left - inset) + index) % (Console.BufferWidth - inset)) let top = p.top + ( (p.left - inset) + index) / (Console.BufferWidth - inset) Cursor.ResetTo(top,left) type ReadLineConsole(complete: (string option * string -> seq)) = let promptColor= Console.ForegroundColor let outColor = Console.ForegroundColor let errorColor = Console.ForegroundColor let history = new History() let mutable complete = complete member x.SetCompletion(v) = complete <- v /// Inset all inputs by this amount member x.Prompt = "> " member x.Prompt2 = "- " member x.Inset = x.Prompt.Length member x.GetOptions(input:string) = /// Tab optionsCache available in current context let optionsCache = new Options() let rec look parenCount i = if i <= 0 then i else match input.Chars(i - 1) with | c when Char.IsLetterOrDigit(c) (* or Char.IsWhiteSpace(c) *) -> look parenCount (i-1) | '.' | '_' -> look parenCount (i-1) | '}' | ')' | ']' -> look (parenCount+1) (i-1) | '(' | '{' | '[' -> look (parenCount-1) (i-1) | _ when parenCount > 0 -> look parenCount (i-1) | _ -> i let start = look 0 input.Length let name = input.Substring(start, input.Length - start); if (name.Trim().Length > 0) then let lastDot = name.LastIndexOf('.'); let attr, pref, root = if (lastDot < 0) then None, name, input.Substring(0, start) else Some(name.Substring(0, lastDot)), name.Substring(lastDot + 1), input.Substring(0, start + lastDot + 1) //printf "attr, pref, root = %s\n" (any_to_string (attr, pref, root)) try complete(attr,pref) |> Seq.filter(fun option -> option.StartsWith(pref,StringComparison.Ordinal)) |> Seq.iter (fun option -> optionsCache.Add(option)) // engine.Evaluate(String.Format("dir({0})", attr)) as IEnumerable; optionsCache.Root <-root; with e -> optionsCache.Clear(); optionsCache,true; else optionsCache,false; member x.MapCharacter(c) : string = match c with | '\x1A'-> "^Z"; | _ -> "^?" member x.GetCharacterSize(c) = if (Char.IsControl(c)) then x.MapCharacter(c).Length else 1 static member TabSize = 4; member x.ReadLine() = let checkLeftEdge(prompt) = let currLeft = Console.CursorLeft if currLeft < x.Inset then if currLeft = 0 then Console.Write (if prompt then x.Prompt2 else String(' ',x.Inset)) Utils.guard(fun () -> Console.CursorLeft <- x.Inset); // The caller writes the primary prompt. If we are reading the 2nd and subsequent lines of the // input we're responsible for writing the secondary prompt. checkLeftEdge(true); /// Cursor anchor - position of !anchor when the routine was called let anchor = ref (Anchor.Current(x.Inset)); /// Length of the output currently rendered on screen. let rendered = ref 0 /// Input has changed, therefore options cache is invalidated. let changed = ref false /// Cache of optionsCache let optionsCache = ref (new Options()) let writeBlank() = Console.Write(' '); checkLeftEdge(false) let writeChar(c) = if Console.CursorTop = Console.BufferHeight - 1 && Console.CursorLeft = Console.BufferWidth - 1 then //printf "bottom right!\n"; anchor := { !anchor with top = (!anchor).top - 1 }; checkLeftEdge(true); if (Char.IsControl(c)) then let s = x.MapCharacter(c) Console.Write(s); rendered := !rendered + s.Length; else Console.Write(c); rendered := !rendered + 1; checkLeftEdge(true) /// The console input buffer. let input = new StringBuilder() /// Current position - index into the input buffer let current = ref 0; let render() = //printf "render\n"; let curr = !current let text = input.ToString() (!anchor).PlaceAt(x.Inset,0); let output = new StringBuilder() let mutable position = -1 for i = 0 to input.Length - 1 do if (i = curr) then position <- output.Length let c = input.Chars(i) if (Char.IsControl(c)) then output.Append(x.MapCharacter(c)) |> ignore; else output.Append(c) |> ignore; if (curr = input.Length) then position <- output.Length; // render the current text, computing a new value for "rendered" let text = output.ToString(); let old_rendered = !rendered rendered := 0; for i = 0 to input.Length - 1 do writeChar(input.Chars(i)); // blank out any dangling old text for i = !rendered to old_rendered - 1 do writeBlank(); (!anchor).PlaceAt(x.Inset,position); render(); let insertChar(c:char) = if (!current = input.Length) then current := !current + 1; input.Append(c) |> ignore; writeChar(c) else input.Insert(!current, c) |> ignore; current := !current + 1; render(); let insertTab() = for i = ReadLineConsole.TabSize - (!current % ReadLineConsole.TabSize) downto 1 do insertChar(' ') let moveLeft() = if (!current > 0 && (!current - 1 < input.Length)) then current := !current - 1 let c = input.Chars(!current) Cursor.Move(x.Inset, - x.GetCharacterSize(c)) let moveRight() = if (!current < input.Length) then let c = input.Chars(!current); current := !current + 1; Cursor.Move(x.Inset, x.GetCharacterSize(c)); let setInput(line:string) = input.Length <- 0; input.Append(line) |> ignore; current := input.Length; render() let tabPress(shift) = let opts,prefix = if !changed then changed := false; x.GetOptions(input.ToString()); else !optionsCache,false optionsCache := opts; if (opts.Count > 0) then let part = if shift then opts.Previous() else opts.Next(); setInput(opts.Root + part); else if (prefix) then Console.Beep(); else insertTab(); let delete() = if (input.Length > 0 && !current < input.Length) then input.Remove(!current, 1) |> ignore; render(); let insert(key: ConsoleKeyInfo) = // REVIEW: is this F6 rewrite required? 0x1A looks like Ctrl-Z. // REVIEW: the Ctrl-Z code is not recognised as EOF by the lexer. // REVIEW: looks like a relic of the port of readline, which is currently removable. let c = if (key.Key = ConsoleKey.F6) then '\x1A' else key.KeyChar let c = ConsoleOptions.readKeyFixup c insertChar(c); let backspace() = if (input.Length > 0 && !current > 0) then input.Remove(!current - 1, 1) |> ignore; current := !current - 1; render(); let enter() = Console.Write("\n"); let line = input.ToString(); if (line = "\x1A") then null else if (line.Length > 0) then history.AddLast(line); line; let rec read() = let key = Console.ReadKey(true) match (key.Key) with | ConsoleKey.Backspace -> backspace(); change() | ConsoleKey.Delete -> delete(); change() | ConsoleKey.Enter -> enter() | ConsoleKey.Tab -> tabPress(key.Modifiers &&& ConsoleModifiers.Shift <> enum 0); read() | ConsoleKey.UpArrow -> setInput(history.Previous()); change() | ConsoleKey.DownArrow -> setInput(history.Next()); change() | ConsoleKey.RightArrow -> moveRight() change() | ConsoleKey.LeftArrow -> moveLeft() change() | ConsoleKey.Escape -> setInput(String.Empty); change() | ConsoleKey.Home -> current := 0; (!anchor).PlaceAt(x.Inset,0) change() | ConsoleKey.End -> current := input.Length; (!anchor).PlaceAt(x.Inset,!rendered); change() | _ -> // Note: If KeyChar=0, the not a proper char, e.g. it could be part of a multi key-press character, // e.g. e-acute is ' and e with the French (Belgium) IME and US Intl KB. // Here: skip KeyChar=0 (except for F6 which maps to 0x1A (ctrl-Z?)). if key.KeyChar <> '\000' || key.Key = ConsoleKey.F6 then insert(key); change() else // Skip and read again. read() and change() = changed := true; read() read()