<<>> <> <> DIRECTORY ConvertUnsafe USING [AppendRope, ToRope], Directory USING [GetNext, Lookup, Error, ignore], IO USING [char, GetOutputStreamRope, GetToken, NUL, IDProc, Put, PutChar, PutRope, rope, RIS, ROS, STREAM], MessageWindow USING [Append, Clear], Process USING [Detach], Rope USING [Cat, Concat, Equal, IsEmpty, Fetch, Find, Flatten, Length, Lower, ROPE, Substr, Text, ToRefText, Upper], RopeInline USING [InlineFlatten], Spell USING [AbortProc, ConfirmProc, defaultModes, extensionList, GeneratorFromProcs, GetMatchingList, GetTheOne, InformProc, IsAPattern, Modes, ROPE, SpellingGenerator, SpellingList], SpellExtras USING [], UnsafeStorage USING [GetSystemUZone], UserProfile USING [CallWhenProfileChanges, ProfileChangedProc, Boolean], WindowManager USING [WaitCursor, UnWaitCursor] ; FileSpellImpl: CEDAR MONITOR IMPORTS ConvertUnsafe, Directory, IO, MessageWindow, Process, Rope, RopeInline, Spell, UnsafeStorage, UserProfile, WindowManager EXPORTS Spell, SpellExtras SHARES Rope -- to be able to say text.length = BEGIN OPEN Spell; <> extensionList: PUBLIC Spell.SpellingList _ LIST["mesa", "cm", "config", "commands", "profile", "df", "doc", "log", "style", "abbreviations", "press", "bcd", "tioga", "mail"]; AddExtension: PUBLIC ENTRY PROC [ext: ROPE] = { ENABLE UNWIND => NULL; FOR l: Spell.SpellingList _ extensionList, l.rest UNTIL l = NIL DO IF Rope.Equal[l.first, ext, FALSE] THEN EXIT; REPEAT FINISHED => extensionList _ CONS[ext, extensionList]; ENDLOOP; }; assumeFirstCharCorrect: BOOL; SetUp: UserProfile.ProfileChangedProc = { assumeFirstCharCorrect _ UserProfile.Boolean["Spell.assumeFirstCharCorrect" , FALSE]; }; GetTheFile: PUBLIC PROC [ unknown: ROPE, defaultExt: ROPE _ NIL, abort: AbortProc _ NIL, confirm: ConfirmProc _ NIL, inform: InformProc _ NIL, modes: Modes _ NIL ] RETURNS [correct: ROPE] = { ENABLE UNWIND => NULL; root, ext, msg: ROPE; unwaitCursor, releaseGenerator: BOOLEAN; IF Rope.IsEmpty[unknown] THEN RETURN[NIL]; IF modes = NIL THEN modes _ Spell.defaultModes; [root, ext, correct] _ SetUpGenerator[unknown: unknown, defaultExt: defaultExt, abort: abort, confirm: confirm, inform: inform, modes: modes]; IF correct # NIL THEN {ReleaseGenerator[]; RETURN[correct]}; -- only mistake was extension. releaseGenerator _ TRUE; -- the generator belongs to this call. it must be released. msg _ Rope.Concat["Trying for spelling correction on ", root]; WindowManager.WaitCursor[]; IF ext # NIL THEN msg _ Rope.Cat[msg, ".", ext]; MessageWindow.Append[message: msg, clearFirst: TRUE]; unwaitCursor _ TRUE; { ENABLE UNWIND => { MessageWindow.Clear[]; IF unwaitCursor THEN WindowManager.UnWaitCursor[]; IF releaseGenerator THEN ReleaseGenerator[]; }; Inform: Spell.InformProc -- [msg: ROPE] -- = { IF releaseGenerator THEN {ReleaseGenerator[]; releaseGenerator _ FALSE}; -- see comment in Confirm below. IF inform = NIL THEN RETURN; IF ext # NIL AND Rope.Find[s1: unknown, s2: "."] # -1 THEN msg _ Rope.Cat[msg, ".", ext]; -- only output extension if extension in original unknown, as opposed to being obtained from defaultExt. inform[msg] }; Confirm: Spell.ConfirmProc -- [msg: ROPE, timeout: INT, defaultConfirm: BOOL] RETURNS[yes: BOOLEAN] -- = { i: INT = Rope.Find[msg, " -> "]; IF releaseGenerator THEN {ReleaseGenerator[]; releaseGenerator _ FALSE}; -- in case user simply ignores this and does something else, spelling corrector is not hung. Need to set releaseGenerator to FALSE so that if he comes back to this later and confirms, when another correction has started, won't mistakenly release the generator again, thereby allowing a third correction to disrupt the second one still in progress. IF i = -1 THEN ERROR; IF Rope.Find[s1: msg, s2: " ", pos1: i + 4] # -1 THEN RETURN[FALSE]; -- more than one candidate. don't bother confirming. CHECK THIS IF confirm = NIL THEN RETURN[FALSE]; unwaitCursor _ FALSE; WindowManager.UnWaitCursor[]; IF ext # NIL AND Rope.Find[s1: unknown, s2: "."] # -1 THEN msg _ Rope.Cat[msg, ".", ext]; -- only output extension if extension in original unknown, as opposed to being obtained from defaultExt. RETURN[confirm[msg, timeout, defaultConfirm]]; }; -- end of confirm <
> IF IsAPattern[unknown] THEN { l: LIST OF ROPE = Spell.GetMatchingList[pattern: root, generator: fileGenerator, abort: abort, confirm: Confirm, inform: Inform, modes: modes]; IF l # NIL AND l.rest = NIL THEN correct _ l.first ELSE correct _ NIL; } ELSE { correct _ GetTheOne[unknown: root, generator: fileGenerator, abort: abort, confirm: IF confirm = NIL THEN NIL ELSE Confirm, inform: IF inform = NIL THEN NIL ELSE Inform, modes: modes]; IF correct = NIL AND fileGenState.stopAfter # LAST[CHARACTER] AND NOT assumeFirstCharCorrect AND releaseGenerator THEN -- no candidates found in corresponding bucket, or else, one was found and user rejected confirmation. now search entire space. TRUSTED { fileGenState.stopAfter _ LAST[CHARACTER]; fileGenState.nextName.length _ 0; fileGenState.currentName.length _ 0; correct _ GetTheOne[unknown: root, generator: fileGenerator, abort: abort, confirm: IF confirm = NIL THEN NIL ELSE Confirm, inform: IF inform = NIL THEN NIL ELSE Inform, modes: modes]; }; }; }; -- end of inner block containing definitions of inform, confirm, etc. IF correct # NIL AND ext # NIL THEN correct _ Rope.Cat[correct, ".", ext]; MessageWindow.Clear[]; IF unwaitCursor THEN WindowManager.UnWaitCursor[]; IF releaseGenerator THEN ReleaseGenerator[]; }; GetFileExtension: PUBLIC PROC [ unknown: Rope.ROPE, spellingList: Spell.SpellingList _ Spell.extensionList, abort: AbortProc _ NIL, confirm: ConfirmProc _ NIL, inform: InformProc _ NIL, modes: Modes _ NIL ] RETURNS [correct: Rope.ROPE] = { len, len1: INT; ext1: ROPE; len _ len1 _ Rope.Length[unknown]; IF len1 > 0 THEN WHILE Rope.Fetch[unknown, len1 - 1] = '$ DO len1 _ len1 - 1; ENDLOOP; IF len1 # len THEN ext1 _ Rope.Substr[base: unknown, len: len1] ELSE ext1 _ unknown; FOR l: SpellingList _ spellingList, l.rest UNTIL l = NIL DO IF Rope.Equal[l.first, ext1, FALSE] THEN RETURN[unknown]; ENDLOOP; correct _ GetTheOne[unknown: ext1, spellingList: spellingList, abort: abort, confirm: confirm, modes: modes]; -- dont print message. want to print it once for entire correction (which may fail). IF correct # NIL THEN { IF len # len1 THEN correct _ Rope.Concat[correct, Rope.Substr[base: unknown, start: len1]]; -- put $'s back }; }; GetMatchingFileList: PUBLIC PROCEDURE [ unknown: ROPE, defaultExt: ROPE _ NIL, abort: AbortProc _ NIL, confirm: ConfirmProc _ NIL, inform: InformProc _ NIL, modes: Modes _ NIL] RETURNS [files: LIST OF ROPE] = { ENABLE UNWIND => NULL; root, ext, correct, msg: ROPE; unwaitCursor, releaseGenerator: BOOLEAN; IF Rope.IsEmpty[unknown] THEN RETURN[NIL]; IF modes = NIL THEN modes _ Spell.defaultModes; [root, ext, correct] _ SetUpGenerator[unknown: unknown, defaultExt: defaultExt, abort: abort, confirm: confirm, inform: inform, modes: modes]; IF correct # NIL THEN {ReleaseGenerator[]; RETURN[LIST[correct]]}; -- only mistake was extension. releaseGenerator _ TRUE; -- the generator belongs to this call. it must be released. IF inform # NIL THEN { msg _ Rope.Concat["Trying for pattern completion on ", root]; IF ext # NIL THEN msg _ Rope.Cat[msg, ".", ext]; MessageWindow.Append[message: msg, clearFirst: TRUE]; WindowManager.WaitCursor[]; unwaitCursor _ TRUE; }; { ENABLE UNWIND => { MessageWindow.Clear[]; IF unwaitCursor THEN WindowManager.UnWaitCursor[]; IF releaseGenerator THEN ReleaseGenerator[]; }; Inform: Spell.InformProc -- [msg: ROPE] -- = { IF releaseGenerator THEN {ReleaseGenerator[]; releaseGenerator _ FALSE}; -- see comments in GetTheOne. IF inform = NIL THEN RETURN; IF ext # NIL AND Rope.Find[s1: unknown, s2: "."] # -1 THEN msg _ AddExt[msg]; -- only output extension if extension in original unknown, as opposed to being obtained from defaultExt. inform[msg] }; Confirm: Spell.ConfirmProc -- [msg: ROPE, timeout: INT, defaultConfirm: BOOL] RETURNS[yes: BOOLEAN] -- = { IF releaseGenerator THEN {ReleaseGenerator[]; releaseGenerator _ FALSE}; -- see comments in GetTheOne. IF confirm = NIL THEN RETURN[FALSE]; unwaitCursor _ FALSE; WindowManager.UnWaitCursor[]; IF ext # NIL AND Rope.Find[s1: unknown, s2: "."] # -1 THEN msg _ AddExt[msg]; -- only output extension if extension in original unknown, as opposed to being obtained from defaultExt. RETURN[confirm[msg, timeout, defaultConfirm]]; }; AddExt: PROC [r: ROPE] RETURNS [ROPE] = { OPEN IO; stream1, stream2: STREAM; token: ROPE; stream1 _ RIS[r]; stream2 _ ROS[]; UNTIL Rope.Length[token _ stream1.GetToken[IO.IDProc]] = 0 DO stream2.PutRope[token]; IF NOT Rope.Equal[token, "->"] THEN stream2.Put[char['.], rope[ext]]; stream2.PutChar[' ]; ENDLOOP; RETURN[stream2.GetOutputStreamRope[]]; }; IF NOT IsAPattern[unknown] THEN { x: ROPE = GetTheOne[unknown: root, generator: fileGenerator, abort: abort, confirm: Confirm, inform: Inform, modes: modes]; IF x # NIL THEN files _ LIST[x] ELSE files _ NIL; } ELSE files _ Spell.GetMatchingList[pattern: root, generator: fileGenerator, abort: abort, confirm: Confirm, inform: Inform, modes: modes]; }; IF files # NIL AND ext # NIL THEN FOR lst: LIST OF ROPE _ files, lst.rest UNTIL lst = NIL DO lst.first _ Rope.Cat[lst.first, ".", ext]; ENDLOOP; MessageWindow.Clear[]; IF unwaitCursor THEN WindowManager.UnWaitCursor[]; IF releaseGenerator THEN ReleaseGenerator[]; }; -- of GetMatchingFileList <> SetUpGenerator: PROC [ unknown: ROPE, defaultExt: ROPE, abort: AbortProc, confirm: ConfirmProc, inform: InformProc, modes: Modes ] RETURNS[root, ext, correct: ROPE] = { noExtension: BOOLEAN _ FALSE; i: INT _ Rope.Find[s1: unknown, s2: "."]; firstChar: CHAR; AcquireGenerator: ENTRY PROC = { IF fileGenerator = NIL THEN -- first time. TRUSTED { s: LONG STRING = UnsafeStorage.GetSystemUZone[].NEW[StringBody[128]]; pathName: ROPE; i, j: INT _ 0; [] _ Directory.GetNext[pathName: "WorkDir>", currentName: "", nextName: s]; pathName _ ConvertUnsafe.ToRope[s]; UNTIL (j _ Rope.Find[s1: pathName, s2: ">", pos1: i]) = -1 DO i _ j + 1; ENDLOOP; s.length _ i; fileGenerator _ GeneratorFromProcs[ generate: FileGenerate, clientData: fileGenState _ NEW[FileGenStateRecord _ [ pathName: s, scratchText: NEW[TEXT[128]], currentName: UnsafeStorage.GetSystemUZone[].NEW[StringBody[128]], nextName: UnsafeStorage.GetSystemUZone[].NEW[StringBody[128]] ]] ] } ELSE WHILE fileGenState.inUse DO WAIT fileGenState.nowFree ENDLOOP; fileGenState.inUse _ TRUE; IF fileCache = NIL THEN fileCache _ NEW[ARRAY CHARACTER['A..'Z] OF ROPE _ ALL[NIL]]; }; <
> root _ unknown; ext _ defaultExt; IF i = -1 THEN { -- unknown does not have an extension IF defaultExt = NIL THEN { IF NOT IsAPattern[unknown] THEN noExtension _ TRUE} -- means only consider those without extension. The IsAPattern check is because Foo* means match everything. ELSE IF IsAPattern[defaultExt] THEN { -- e.g. "*press", meaning match .press and .mesa.press. only way to handle this is to treat the whole thing as a unit. root _ Rope.Cat[root, ".", defaultExt]; ext _ NIL; } } ELSE IF i = Rope.Length[unknown] - 1 -- i.e. of form foo. THEN {root _ Rope.Substr[base: unknown, len: i]; ext _ ""} ELSE IF IsAPattern[ext _ Rope.Substr[base: unknown, start: i + 1]] THEN -- treat whole name as unit {ext _ NIL; root _ unknown} ELSE { ext1: ROPE; root _ Rope.Substr[base: unknown, len: i]; ext1 _ GetFileExtension[unknown: ext, abort: abort, confirm: confirm, modes: modes]; IF ext1 = NIL THEN {root _ unknown; ext _ NIL} -- the extension is not recognized, so treat whole name as a unit. ELSE IF ext1 # ext THEN { -- was corrected r: ROPE = Rope.Cat[root, ".", ext1]; IF CheckForFile[r] THEN { -- extension was the only thing misspelled. return now correct name. IF inform # NIL THEN inform[Rope.Cat[unknown, " -> ", r]]; correct _ r; RETURN; } ELSE ext _ ext1;-- ext is known. spelling correct on root only. }; }; AcquireGenerator[]; fileGenState.ext _ RopeInline.InlineFlatten[ext]; fileGenState.noExtension _ noExtension; fileGenState.stopAfter _ LAST[CHARACTER]; TRUSTED {fileGenState.nextName.length _ 0; fileGenState.currentName.length _ 0}; fileGenState.scratchText.length _ 0; IF (firstChar _ Rope.Upper[Rope.Fetch[unknown, 0]]) IN ['A..'Z] THEN { fileGenState.stopAfter _ firstChar; FOR c: CHAR DECREASING IN ['A..firstChar] DO IF useFileCache AND NOT Rope.IsEmpty[fileCache[c]] AND CheckForFile[fileCache[c]] THEN TRUSTED { ConvertUnsafe.AppendRope[to: fileGenState.nextName, from: fileCache[c]]; EXIT; }; ENDLOOP; }; }; -- of SetUpGenerator ReleaseGenerator: ENTRY PROC = {fileGenState.inUse _ FALSE; NOTIFY fileGenState.nowFree}; <> FileGenState: TYPE = REF FileGenStateRecord _ NIL; FileGenStateRecord: TYPE = RECORD[ pathName, currentName, nextName: LONG STRING, scratchText: REF TEXT, ext: Rope.Text, noExtension: BOOLEAN _ FALSE, inUse: BOOL _ FALSE, stopAfter: CHARACTER _ IO.NUL, nowFree: CONDITION ]; fileGenerator: Spell.SpellingGenerator _ NIL; fileGenState: FileGenState _ NIL; fileCache: REF ARRAY CHARACTER['A..'Z] OF ROPE _ NIL; -- contains the last file just previous (in alphabetic order) to the indicated character. e.g. for B might be AtomsPrivate.BCD useFileCache: BOOL _ TRUE; buildFileCache: BOOL _ TRUE; FileGenerate: PROC [self: SpellingGenerator] RETURNS [REF TEXT] = TRUSTED { state: FileGenState _ NARROW[self.clientData]; ext: Rope.Text = state.ext; noExtension: BOOL = state.noExtension; pathLen: NAT = state.pathName.length; currentLen: NAT; len: NAT; s: LONG STRING; DO extDidntMatch: BOOL _ FALSE; s _ state.currentName; state.currentName _ state.nextName; state.nextName _ s; [] _ Directory.GetNext[pathName: state.pathName, currentName: state.currentName, nextName: state.nextName]; IF state.nextName.length = 0 THEN RETURN[NIL]; IF state.currentName.length = 0 OR Rope.Upper[state.nextName[pathLen]] > Rope.Upper[state.currentName[pathLen]] THEN -- new letter (assumes enumeration in alphabetic order) { cache: ROPE; len: INT; currentChar: CHAR = Rope.Upper[state.nextName[pathLen]]; IF currentChar > fileGenState.stopAfter THEN RETURN[NIL]; -- says that if you are doing a pattern match and first character is not a *, ok to stop as soon as you have passed that character. <> IF currentChar IN ['A..'Z] THEN { cache _ fileCache[currentChar]; len _ Rope.Length[cache]; FOR i: INT IN [0..state.currentName.length) DO IF i >= len OR state.currentName[i] # Rope.Fetch[cache, i] THEN { IF buildFileCache THEN fileCache[currentChar] _ ConvertUnsafe.ToRope[state.currentName]; -- insert in cache. EXIT; }; ENDLOOP; } }; currentLen _ state.nextName.length; len _ currentLen - pathLen; <> FOR i: NAT IN [pathLen..currentLen) DO char: CHARACTER _ state.nextName[i]; IF char # '. THEN NULL ELSE IF noExtension THEN GOTO Fail ELSE IF ext # NIL THEN -- NIL means look at all extensions, i.e. treat the entire name as a unit. { -- compare extension with ext. If not equal, do not consider this candidate. i1: NAT _ i + 1; FOR j: NAT IN [0..ext.length) DO IF i1 = currentLen THEN GOTO Fail; -- unaccounted for characters in target extension IF Rope.Lower[ext[j]] # Rope.Lower[state.nextName[i1]] THEN GOTO MaybeFail; -- target = .press and ext (at this point) is mesa.press i1 _ i1 + 1; REPEAT FINISHED => IF i1 < currentLen THEN GOTO MaybeFail; -- unaccounted for character in unknown extension, e.g. .press.press where target is .press ENDLOOP; len _ i - pathLen; extDidntMatch _ FALSE; EXIT; EXITS MaybeFail => extDidntMatch _ TRUE; }; state.scratchText[i - pathLen] _ char; REPEAT Fail => LOOP; FINISHED => IF (ext # NIL AND NOT noExtension) OR extDidntMatch THEN LOOP; -- to get here, either this file doesnt have an extension, or else ext = NIL, i.e. consider all files. This test says that if an extension is required, then it must be the case that this file does not have one, so reject it. ENDLOOP; state.scratchText.length _ len; RETURN[state.scratchText]; ENDLOOP; }; LoadCache: PROC = { [] _ SetUpGenerator[unknown: "zzz", defaultExt: NIL, abort: NIL, confirm: NIL, inform: NIL, modes: NIL]; DO IF FileGenerate[fileGenerator] = NIL THEN EXIT; ENDLOOP; ReleaseGenerator[] }; <> CheckForFile: PROC [file: ROPE] RETURNS [found: BOOLEAN] = TRUSTED { fName: LONG STRING; fName _ LOOPHOLE[Rope.ToRefText[file]]; found _ TRUE; IF Rope.Length[file] = 0 THEN RETURN[FALSE]; [] _ Directory.Lookup[fileName: fName, permissions: Directory.ignore ! Directory.Error => {found _ FALSE; CONTINUE} ]; }; TRUSTED {Process.Detach[FORK LoadCache[]]}; UserProfile.CallWhenProfileChanges[SetUp]; END. June 21, 1982 1:09 pm made patternMatch one of enumerated type of spelling classes.