unit ce_synmemo; {$I ce_defines.inc} interface uses Classes, SysUtils, SynEdit, ce_d2syn, ce_txtsyn ,SynEditHighlighter, controls, lcltype, LazSynEditText, SynEditKeyCmds, SynHighlighterLFM, SynEditMouseCmds, SynEditFoldedView, crc, ce_common, ce_observer, ce_writableComponent, Forms, graphics, ExtCtrls; type TCESynMemo = class; // SImple THintWindow descendant allowing the font size to be in sync with the editor. TCEEditorHintWindow = class(THintWindow) public class var FontSize: Integer; function CalcHintRect(MaxWidth: Integer; const AHint: String; AData: Pointer): TRect; override; end; // Stores the state of a particulat source code folding. TCEFoldCache = class(TCollectionItem) private fCollapsed: boolean; fLineIndex: Integer; fNestedIndex: Integer; published property isCollapsed: boolean read fCollapsed write fCollapsed; property lineIndex: Integer read fLineIndex write fLineIndex; property nestedIndex: Integer read fNestedIndex write fNestedIndex; end; // Stores the state of a document between two cessions. TCESynMemoCache = class(TWritableLfmTextComponent) private fMemo: TCESynMemo; fFolds: TCollection; fCaretPosition: Integer; fSelectionEnd: Integer; fFontSize: Integer; fSourceFilename: string; procedure setFolds(someFolds: TCollection); published property caretPosition: Integer read fCaretPosition write fCaretPosition; property sourceFilename: string read fSourceFilename write fSourceFilename; property folds: TCollection read fFolds write setFolds; property selectionEnd: Integer read fSelectionEnd write fSelectionEnd; property fontSize: Integer read fFontSize write fFontSize; public constructor create(aComponent: TComponent); override; destructor destroy; override; // procedure beforeSave; override; procedure afterLoad; override; procedure save; procedure load; end; // buffer of caret positions allowing to jump quickly to the most recent locations. TCESynMemoPositions = class private fPos: Integer; fMax: Integer; fList: TFPList; fMemo: TCustomSynEdit; public constructor create(aMemo: TCustomSynEdit); destructor destroy; override; procedure store; procedure back; procedure next; end; TCESynMemo = class(TSynEdit) private fFilename: string; fModified: boolean; fFileDate: double; fIsDSource: boolean; fIsTxtFile: boolean; fIsConfig: boolean; fIdentifier: string; fTempFileName: string; fMultiDocSubject: TCECustomSubject; fDefaultFontSize: Integer; fPositions: TCESynMemoPositions; fMousePos: TPoint; fCallTipWin: TCEEditorHintWindow; fDDocWin: TCEEditorHintWindow; fIdleTimer: TIdleTimer; fMayHint: boolean; function getMouseStart: Integer; procedure changeNotify(Sender: TObject); procedure identifierToD2Syn; procedure saveCache; procedure loadCache; procedure setDefaultFontSize(aValue: Integer); procedure hintWinClick(sender: TObject); procedure EditorIdle(sender: TObject); procedure InitHintWins; protected procedure SetVisible(Value: Boolean); override; procedure SetHighlighter(const Value: TSynCustomHighlighter); override; procedure KeyDown(var Key: Word; Shift: TShiftState); override; procedure KeyUp(var Key: Word; Shift: TShiftState); override; procedure MouseMove(Shift: TShiftState; X, Y: Integer); override; procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y:Integer); override; procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y:Integer); override; published property defaultFontSize: Integer read fDefaultFontSize write setDefaultFontSize; public constructor Create(aOwner: TComponent); override; destructor destroy; override; procedure setFocus; override; // procedure checkFileDate; procedure loadFromFile(const aFilename: string); procedure saveToFile(const aFilename: string); procedure save; procedure saveTempFile; // property Identifier: string read fIdentifier; property fileName: string read fFilename; property modified: boolean read fModified; property tempFilename: string read fTempFileName; // property isDSource: boolean read fIsDSource; property isProjectSource: boolean read fIsConfig; property TextView; // property MouseStart: Integer read getMouseStart; end; var D2Syn: TSynD2Syn; LfmSyn: TSynLfmSyn; TxtSyn: TSynTxtSyn; implementation uses ce_interfaces, ce_staticmacro, ce_dcd, SynEditHighlighterFoldBase; function TCEEditorHintWindow.CalcHintRect(MaxWidth: Integer; const AHint: String; AData: Pointer): TRect; begin Font.Size:= FontSize; result := inherited CalcHintRect(MaxWidth, AHint, AData); end; {$REGION TCESynMemoCache -------------------------------------------------------} constructor TCESynMemoCache.create(aComponent: TComponent); begin inherited create(nil); if (aComponent is TCESynMemo) then fMemo := TCESynMemo(aComponent); fFolds := TCollection.Create(TCEFoldCache); end; destructor TCESynMemoCache.destroy; begin fFolds.Free; inherited; end; procedure TCESynMemoCache.setFolds(someFolds: TCollection); begin fFolds.Assign(someFolds); end; procedure TCESynMemoCache.beforeSave; var i, start, prev: Integer; itm : TCEFoldCache; begin if fMemo = nil then exit; // fCaretPosition := fMemo.SelStart; fSourceFilename := fMemo.fileName; fSelectionEnd := fMemo.SelEnd; fFontSize := fMemo.Font.Size; TCEEditorHintWindow.FontSize := fMemo.Font.Size; // // TODO-cEditor Cache: >nested< folding persistence // cf. other ways: http://forum.lazarus.freepascal.org/index.php?topic=26748.msg164722#msg164722 prev := fMemo.Lines.Count-1; for i := fMemo.Lines.Count-1 downto 0 do begin // - CollapsedLineForFoldAtLine() does not handle the sub-folding. // - TextView visibility is increased so this is not the standard way of getting the infos. start := fMemo.TextView.CollapsedLineForFoldAtLine(i); if start = -1 then continue; if start = prev then continue; prev := start; itm := TCEFoldCache(fFolds.Add); itm.isCollapsed := true; itm.fLineIndex := start; end; end; procedure TCESynMemoCache.afterLoad; var i: integer; itm : TCEFoldCache; begin if fMemo = nil then exit; // if fFontSize > 0 then fMemo.Font.Size := fFontSize; // Currently collisions are not handled. if fMemo.fileName <> fSourceFilename then exit; // for i := 0 to fFolds.Count-1 do begin itm := TCEFoldCache(fFolds.Items[i]); if not itm.isCollapsed then continue; fMemo.TextView.FoldAtLine(itm.lineIndex-1); end; // fMemo.SelStart := fCaretPosition; fMemo.SelEnd := fSelectionEnd; end; {$IFDEF DEBUG}{$R-}{$ENDIF} procedure TCESynMemoCache.save; var fname: string; tempn: string; chksm: Cardinal; begin tempn := fMemo.fileName; if tempn = fMemo.tempFilename then exit; if not fileExists(tempn) then exit; // fname := getCoeditDocPath + 'editorcache' + DirectorySeparator; ForceDirectories(fname); chksm := crc32(0, nil, 0); chksm := crc32(chksm, @tempn[1], length(tempn)); fname := fname + format('%.8X.txt', [chksm]); saveToFile(fname); end; procedure TCESynMemoCache.load; var fname: string; tempn: string; chksm: Cardinal; begin tempn := fMemo.fileName; if not fileExists(tempn) then exit; // fname := getCoeditDocPath + 'editorcache' + DirectorySeparator; chksm := crc32(0, nil, 0); chksm := crc32(chksm, @tempn[1], length(tempn)); fname := fname + format('%.8X.txt', [chksm]); // if not fileExists(fname) then exit; loadFromFile(fname); end; {$IFDEF DEBUG}{$R+}{$ENDIF} {$ENDREGION} {$REGION TCESynMemoPositions ---------------------------------------------------} constructor TCESynMemoPositions.create(aMemo: TCustomSynEdit); begin fList := TFPList.Create; fMax := 20; fMemo := aMemo; fPos := -1; end; destructor TCESynMemoPositions.destroy; begin fList.Free; inherited; end; procedure TCESynMemoPositions.back; begin Inc(fPos); {$HINTS OFF} if fPos < fList.Count then fMemo.CaretY := NativeInt(fList.Items[fPos]) {$HINTS ON} else Dec(fPos); end; procedure TCESynMemoPositions.next; begin Dec(fPos); {$HINTS OFF} if fPos > -1 then fMemo.CaretY := NativeInt(fList.Items[fPos]) {$HINTS ON} else Inc(fPos); end; procedure TCESynMemoPositions.store; var delta: NativeInt; const thresh = 6; begin fPos := 0; {$HINTS OFF}{$WARNINGS OFF} if fList.Count > 0 then begin delta := fMemo.CaretY - NativeInt(fList.Items[fPos]); if (delta > -thresh) and (delta < thresh) then exit; end; fList.Insert(0, Pointer(NativeInt(fMemo.CaretY))); {$HINTS ON}{$WARNINGS ON} while fList.Count > fMax do fList.Delete(fList.Count-1); end; {$ENDREGION --------------------------------------------------------------------} {$REGION TCESynMemo ------------------------------------------------------------} constructor TCESynMemo.Create(aOwner: TComponent); begin inherited; InitHintWins; self.ShowHint:=false; SetDefaultKeystrokes; // not called in inherited if owner = nil ! fDefaultFontSize := 10; fIdleTimer := TIdleTimer.Create(self); fIdleTimer.OnTimer := @EditorIdle; Gutter.LineNumberPart.ShowOnlyLineNumbersMultiplesOf := 5; Gutter.LineNumberPart.MarkupInfo.Foreground := clGray; Gutter.SeparatorPart.LineOffset := 1; Gutter.SeparatorPart.LineWidth := 1; Gutter.SeparatorPart.MarkupInfo.Foreground := clGray; Gutter.CodeFoldPart.MarkupInfo.Foreground := clGray; BracketMatchColor.Foreground:=clRed; // MouseLinkColor.Style:= [fsUnderline]; with MouseActions.Add do begin Command := emcMouseLink; shift := [ssCtrl]; ShiftMask := [ssCtrl]; end; // Highlighter := D2Syn; // fTempFileName := GetTempDir(false) + 'temp_' + uniqueObjStr(self) + '.d'; fFilename := ''; fModified := false; //ShowHint := true; TextBuffer.AddNotifyHandler(senrUndoRedoAdded, @changeNotify); // fPositions := TCESynMemoPositions.create(self); fMultiDocSubject := TCEMultiDocSubject.create; subjDocNew(TCEMultiDocSubject(fMultiDocSubject), self); end; destructor TCESynMemo.destroy; begin saveCache; // subjDocClosing(TCEMultiDocSubject(fMultiDocSubject), self); fMultiDocSubject.Free; fPositions.Free; // if fileExists(fTempFileName) then sysutils.DeleteFile(fTempFileName); inherited; end; procedure TCESynMemo.setDefaultFontSize(aValue: Integer); var old: Integer; begin old := Font.Size; if aValue < 5 then aValue := 5; fDefaultFontSize:= aValue; if Font.Size = old then Font.Size := fDefaultFontSize; end; procedure TCESynMemo.setFocus; begin inherited; checkFileDate; identifierToD2Syn; subjDocFocused(TCEMultiDocSubject(fMultiDocSubject), self); end; procedure TCESynMemo.SetVisible(Value: Boolean); begin inherited; if Value then setFocus; end; procedure TCESynMemo.hintWinClick(sender: TObject); begin with THintWindow(sender) do Hide; end; procedure TCESynMemo.InitHintWins; begin if fCallTipWin = nil then begin fCallTipWin := TCEEditorHintWindow.Create(self); fCallTipWin.Color := clInfoBk + $01010100; fCallTipWin.Font.Color:= clInfoText; fCallTipWin.OnClick:= @hintWinClick; end; if fDDocWin = nil then begin fDDocWin := TCEEditorHintWindow.Create(self); fDDocWin.Color := clInfoBk + $01010100; fDDocWin.Font.Color:= clInfoText; fDDocWin.OnClick:= @hintWinClick; end; end; procedure TCESynMemo.EditorIdle(sender: TObject); var str: string; begin if not Visible then exit; if not isDSource then exit; // if not fMayHint then exit; if Identifier = '' then exit; DcdWrapper.getDdocFromCursor(str); // if (length(str) > 0) then if str[1] = #13 then str := str[2..length(str)]; if (length(str) > 0) then if str[1] = #10 then str := str[2..length(str)]; // if str <> '' then begin fDDocWin.FontSize := Font.Size; fDDocWin.Font.Size:=Font.Size; fDDocWin.HintRect := fCallTipWin.CalcHintRect(0, str, nil); fDDocWin.OffsetHintRect(mouse.CursorPos, Font.Size); fDDocWin.ActivateHint(fDDocWin.HintRect, str); end; end; procedure TCESynMemo.SetHighlighter(const Value: TSynCustomHighlighter); begin inherited; fIsDSource := Highlighter = D2Syn; fIsConfig := Highlighter = LfmSyn; fIsTxtFile := Highlighter = TxtSyn; end; procedure TCESynMemo.identifierToD2Syn; begin fIdentifier := GetWordAtRowCol(LogicalCaretXY); if fIsDSource then D2Syn.CurrentIdentifier := fIdentifier else if fIsTxtFile then TxtSyn.CurrIdent := fIdentifier; end; procedure TCESynMemo.changeNotify(Sender: TObject); begin identifierToD2Syn; fModified := true; fPositions.store; subjDocChanged(TCEMultiDocSubject(fMultiDocSubject), self); end; procedure TCESynMemo.loadFromFile(const aFilename: string); var ext: string; begin ext := extractFileExt(aFilename); if dExtList.IndexOf(ext) = -1 then Highlighter := TxtSyn; Lines.LoadFromFile(aFilename); fFilename := aFilename; FileAge(fFilename, fFileDate); // loadCache; // fModified := false; if Showing then setFocus; subjDocChanged(TCEMultiDocSubject(fMultiDocSubject), self); end; procedure TCESynMemo.saveToFile(const aFilename: string); var ext: string; begin Lines.SaveToFile(aFilename); fFilename := aFilename; ext := extractFileExt(aFilename); if dExtList.IndexOf(ext) <> -1 then Highlighter := D2Syn; FileAge(fFilename, fFileDate); fModified := false; if fFilename <> fTempFileName then subjDocChanged(TCEMultiDocSubject(fMultiDocSubject), self); end; procedure TCESynMemo.save; begin Lines.SaveToFile(fFilename); FileAge(fFilename, fFileDate); fModified := false; if fFilename <> fTempFileName then subjDocChanged(TCEMultiDocSubject(fMultiDocSubject), self); end; procedure TCESynMemo.saveTempFile; begin saveToFile(fTempFileName); fModified := false; end; procedure TCESynMemo.saveCache; var cache: TCESynMemoCache; begin cache := TCESynMemoCache.create(self); try cache.save; finally cache.free; end; end; procedure TCESynMemo.loadCache; var cache: TCESynMemoCache; begin cache := TCESynMemoCache.create(self); try cache.load; finally cache.free; end; end; procedure TCESynMemo.checkFileDate; var newDate: double; begin if fFilename = fTempFileName then exit; if not FileAge(fFilename, newDate) then exit; if fFileDate = newDate then exit; if fFileDate <> 0.0 then begin if dlgOkCancel(format('"%s" has been modified by another program, load the new version ?', [shortenPath(fFilename, 25)])) = mrOk then begin Lines.LoadFromFile(fFilename); fModified := false; end; end; fFileDate := newDate; end; procedure TCESynMemo.KeyDown(var Key: Word; Shift: TShiftState); begin inherited; identifierToD2Syn; if not (Shift = [ssCtrl]) then exit; // case Key of VK_ADD: if Font.Size < 50 then Font.Size := Font.Size + 1; VK_SUBTRACT: if Font.Size > 3 then Font.Size := Font.Size - 1; VK_DECIMAL: Font.Size := fDefaultFontSize; end; TCEEditorHintWindow.FontSize := Font.Size; fDDocWin.Hide; end; procedure TCESynMemo.KeyUp(var Key: Word; Shift: TShiftState); var str: string; begin if Key in [VK_PRIOR, VK_NEXT, Vk_UP] then fPositions.store; inherited; // if StaticEditorMacro.automatic then StaticEditorMacro.Execute; // if Key = 53 then begin if fCallTipWin = nil then begin fCallTipWin := TCEEditorHintWindow.Create(self); fCallTipWin.Color := clInfoBk + $01010100; end; DcdWrapper.getCallTip(str); if str <> '' then begin fCallTipWin.FontSize := Font.Size; fCallTipWin.HintRect := fCallTipWin.CalcHintRect(0, str, nil); fCallTipWin.OffsetHintRect(point(CaretXPix, CaretYPix), Font.Size); fCallTipWin.ActivateHint(str); end; end else fCallTipWin.Hide; end; function TCESynMemo.getMouseStart: Integer; var i, le: Integer; begin result := 0; le := getLineEndingLength(fFilename); for i:= 0 to fMousePos.y-2 do result += length(Lines.Strings[i]) + le; result += fMousePos.x; end; procedure TCESynMemo.MouseMove(Shift: TShiftState; X, Y: Integer); begin fDDocWin.Hide; fCallTipWin.Hide; fMayHint := ((Y > 10) or (Y < -10)) and ((X > 10) or (X < -10)); fMousePos := PixelsToRowColumn(Point(X,Y)); inherited; if ssLeft in Shift then identifierToD2Syn; end; procedure TCESynMemo.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y:Integer); begin inherited; identifierToD2Syn; fDDocWin.Hide; fCallTipWin.Hide; end; procedure TCESynMemo.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y:Integer); begin inherited; if (Button = mbMiddle) and (Shift = [ssCtrl]) then Font.Size := fDefaultFontSize //TODO-cLCL&LAZ-specific: test this feature under gtk2/linux on next release, should work else if Button = mbExtra1 then fPositions.back else if Button = mbExtra2 then fPositions.next else if Button = mbLeft then fPositions.store; end; {$ENDREGION --------------------------------------------------------------------} initialization D2Syn := TSynD2Syn.create(nil); LfmSyn := TSynLFMSyn.Create(nil); TxtSyn := TSynTxtSyn.create(nil); // LfmSyn.KeyAttri.Foreground := clNavy; LfmSyn.KeyAttri.Style := [fsBold]; LfmSyn.NumberAttri.Foreground := clMaroon; LfmSyn.StringAttri.Foreground := clBlue; // TCEEditorHintWindow.FontSize := 10; finalization D2Syn.Free; LfmSyn.Free; TxtSyn.Free; end.